diff --git a/ADMIN_README.md b/ADMIN_README.md new file mode 100644 index 0000000..38ad711 --- /dev/null +++ b/ADMIN_README.md @@ -0,0 +1,353 @@ +# ๐Ÿš€ Doogle Admin Panel + +A comprehensive administration panel for the Doogle search engine with advanced features for managing content, users, analytics, and system monitoring. + +## โœจ Features + +### ๐Ÿ” **Secure Authentication System** +- Role-based access control (Super Admin, Admin, User) +- Session management with automatic expiration +- Brute force protection with account lockout +- Activity logging for all admin actions + +### ๐Ÿ“Š **Advanced Dashboard** +- Real-time statistics and metrics +- Visual charts and graphs +- System health monitoring +- Recent activity tracking + +### ๐Ÿ•ท๏ธ **Crawl Management** +- Add and monitor web crawling jobs +- Priority-based crawling queue +- Real-time crawl progress tracking +- Crawl job status management (pending, running, completed, failed, paused) + +### ๐Ÿ“ˆ **Search Analytics** +- Detailed search statistics and trends +- Top search terms analysis +- Click-through rate tracking +- Response time monitoring +- Failed search identification + +### ๐Ÿ—‚๏ธ **Content Management** +- Browse and manage indexed sites +- Image management with broken image detection +- Bulk operations for content cleanup +- Search and filter capabilities + +### ๐Ÿ‘ฅ **User Management** +- Create and manage admin accounts +- Role assignment and permissions +- User activity monitoring +- Account status management + +### ๐Ÿ“ **System Logs & Monitoring** +- Comprehensive system logging +- Real-time log filtering and search +- System health metrics +- Database performance monitoring + +### ๐Ÿ› ๏ธ **Database Tools** +- Database optimization tools +- Search index rebuilding +- Automated cleanup utilities +- Backup creation +- System maintenance tools + +## ๐Ÿš€ Installation & Setup + +### 1. **Database Setup** + +Run the admin setup SQL to create the necessary tables: + +```bash +mysql -u your_username -p your_database < admin-setup.sql +``` + +Or execute the SQL commands in phpMyAdmin: +- Import `admin-setup.sql` in your database +- This will create admin tables and add necessary indexes + +### 2. **File Structure** + +Ensure your admin panel files are in the `/admin/` directory: + +``` +/workspace/ +โ”œโ”€โ”€ admin/ +โ”‚ โ”œโ”€โ”€ config.php # Admin configuration +โ”‚ โ”œโ”€โ”€ login.php # Login page +โ”‚ โ”œโ”€โ”€ index.php # Main dashboard +โ”‚ โ”œโ”€โ”€ crawl-management.php # Crawl job management +โ”‚ โ”œโ”€โ”€ search-analytics.php # Analytics dashboard +โ”‚ โ”œโ”€โ”€ content-management.php # Content moderation +โ”‚ โ”œโ”€โ”€ user-management.php # User administration +โ”‚ โ”œโ”€โ”€ system-logs.php # System monitoring +โ”‚ โ”œโ”€โ”€ settings.php # Database tools +โ”‚ โ””โ”€โ”€ logout.php # Logout handler +โ”œโ”€โ”€ classes/ +โ”‚ โ””โ”€โ”€ AnalyticsTracker.php # Analytics tracking class +โ”œโ”€โ”€ ajax/ +โ”‚ โ””โ”€โ”€ track-click.php # Click tracking endpoint +โ””โ”€โ”€ admin-setup.sql # Database setup script +``` + +### 3. **Default Admin Account** + +A default super admin account is created during setup: + +- **Username**: `admin` +- **Password**: `admin123` +- **โš ๏ธ IMPORTANT**: Change this password immediately after first login! + +### 4. **Configuration** + +Update `/admin/config.php` if needed: + +```php +// Session timeout (default: 1 hour) +define('ADMIN_SESSION_TIMEOUT', 3600); + +// Max login attempts before lockout +define('ADMIN_MAX_LOGIN_ATTEMPTS', 5); + +// Lockout duration (default: 15 minutes) +define('ADMIN_LOCKOUT_TIME', 900); +``` + +## ๐Ÿ”ง Usage Guide + +### **Accessing the Admin Panel** + +1. Navigate to `http://your-domain.com/admin/login.php` +2. Login with your admin credentials +3. You'll be redirected to the main dashboard + +### **Dashboard Overview** + +The main dashboard provides: +- **Site Statistics**: Total indexed sites and daily additions +- **Image Statistics**: Total images, broken images count +- **Search Metrics**: Daily searches and popular terms +- **System Health**: Recent activity and error counts + +### **Managing Crawl Jobs** + +1. Go to **Crawl Management** +2. Click "Add New Crawl" to create crawling jobs +3. Set priority levels (Low, Normal, High) +4. Monitor crawl progress in real-time +5. Manage job status (pause, resume, stop) + +### **Analytics & Reporting** + +1. Visit **Search Analytics** for detailed insights: + - Search volume trends + - Most popular search terms + - Click-through rates + - Response time analysis + - Failed search identification + +2. Use date filters to analyze specific time periods +3. Export data for further analysis + +### **Content Moderation** + +1. **Sites Management**: + - Browse all indexed sites + - Search and filter content + - Delete inappropriate or broken sites + +2. **Images Management**: + - View all indexed images with previews + - Mark broken images + - Bulk delete broken images + - Filter by status (working/broken) + +### **User Administration** + +**Super Admin Only Features**: +- Create new admin accounts +- Assign roles (User, Admin, Super Admin) +- Manage user status (Active, Inactive, Banned) +- View login activity +- Reset user passwords + +### **System Monitoring** + +1. **System Logs**: + - View all system activities + - Filter by log level (Info, Warning, Error, Critical) + - Filter by category (Auth, Crawl, Content, etc.) + - Real-time log monitoring + +2. **Health Metrics**: + - Database size monitoring + - Recent error counts + - System resource usage + - Performance metrics + +### **Database Maintenance** + +**Super Admin Tools**: +- **Optimize Database**: Improve performance by optimizing tables +- **Rebuild Indexes**: Enhance search performance +- **Clean Old Logs**: Remove old system logs to save space +- **Session Cleanup**: Remove expired admin sessions +- **Create Backups**: Generate database backups + +## ๐Ÿ”’ Security Features + +### **Authentication Security** +- Password hashing using PHP's `password_hash()` +- Session token validation +- IP address tracking +- User agent verification +- Automatic session expiration + +### **Access Control** +- Role-based permissions +- Route protection middleware +- Super Admin restricted features +- Activity logging for audit trails + +### **Brute Force Protection** +- Failed login attempt tracking +- Temporary account lockouts +- IP-based rate limiting +- Security event logging + +## ๐Ÿ“Š Analytics Integration + +The admin panel automatically tracks: + +### **Search Analytics** +- Every search query with metadata +- Response times and result counts +- User interaction patterns +- Click-through rates + +### **System Analytics** +- Admin login activity +- Crawl job performance +- Content management actions +- System errors and warnings + +## ๐Ÿ› ๏ธ Customization + +### **Adding Custom Metrics** + +1. Extend the `AnalyticsTracker` class: + +```php +class CustomAnalytics extends AnalyticsTracker { + public function trackCustomEvent($event, $data) { + // Your custom tracking logic + } +} +``` + +2. Add new dashboard widgets in `index.php` +3. Create custom report pages + +### **Theme Customization** + +The admin panel uses Bootstrap 5 with custom CSS. Modify the ` + + +
+
+ + + + +
+ + + +
+ +
+ + + +
+ + + + + + + +
+
+
+
+
Total Sites Indexed
+
+
+
+ +
+
+
+
+
Total Images
+
+
+
+
+
+
Working Images
+
+
+
+
+
+
Broken Images
+
+
+
+ + + + + 0): ?> +
+ +
+ + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
IDTitleURLDescriptionClicksCreatedActionsIDPreviewAlt TextImage URLSource SiteStatusClicksActions
+ + + + + + + 100 ? '...' : ''); ?> + + + + <?php echo htmlspecialchars($item['alt']); ?> + + + + + + + + + + + + + Broken + + Working + + +
+ + + + + + +
+
+ +

No found

+ + + Clear Search + + +
+
+ + + 1): ?> + + +
+
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/admin/crawl-management.php b/admin/crawl-management.php new file mode 100644 index 0000000..a9f4b06 --- /dev/null +++ b/admin/crawl-management.php @@ -0,0 +1,483 @@ +requireAuth(); + +$message = ''; +$messageType = ''; + +// Handle form submissions +if ($_POST) { + if (isset($_POST['action'])) { + switch ($_POST['action']) { + case 'add_crawl': + if (!empty($_POST['url'])) { + try { + $stmt = $con->prepare("INSERT INTO crawl_jobs (url, priority, created_by) VALUES (?, ?, ?)"); + $priority = $_POST['priority'] ?? 'normal'; + $stmt->execute([$_POST['url'], $priority, $_SESSION['admin_id']]); + + $adminAuth->logActivity('info', 'crawl', 'New crawl job added', ['url' => $_POST['url']]); + $message = 'Crawl job added successfully!'; + $messageType = 'success'; + } catch (Exception $e) { + $message = 'Error adding crawl job: ' . $e->getMessage(); + $messageType = 'danger'; + } + } + break; + + case 'update_status': + if (!empty($_POST['job_id']) && !empty($_POST['status'])) { + try { + $stmt = $con->prepare("UPDATE crawl_jobs SET status = ?, updated_at = NOW() WHERE id = ?"); + $stmt->execute([$_POST['status'], $_POST['job_id']]); + + $adminAuth->logActivity('info', 'crawl', 'Crawl job status updated', ['job_id' => $_POST['job_id'], 'status' => $_POST['status']]); + $message = 'Crawl job status updated!'; + $messageType = 'success'; + } catch (Exception $e) { + $message = 'Error updating crawl job: ' . $e->getMessage(); + $messageType = 'danger'; + } + } + break; + + case 'delete_crawl': + if (!empty($_POST['job_id'])) { + try { + $stmt = $con->prepare("DELETE FROM crawl_jobs WHERE id = ?"); + $stmt->execute([$_POST['job_id']]); + + $adminAuth->logActivity('warning', 'crawl', 'Crawl job deleted', ['job_id' => $_POST['job_id']]); + $message = 'Crawl job deleted!'; + $messageType = 'success'; + } catch (Exception $e) { + $message = 'Error deleting crawl job: ' . $e->getMessage(); + $messageType = 'danger'; + } + } + break; + } + } +} + +// Get crawl jobs with pagination +$page = isset($_GET['page']) ? (int)$_GET['page'] : 1; +$limit = 20; +$offset = ($page - 1) * $limit; + +$filter = $_GET['filter'] ?? 'all'; +$whereClause = ''; +$params = []; + +if ($filter !== 'all') { + $whereClause = 'WHERE status = ?'; + $params[] = $filter; +} + +try { + // Get total count + $countStmt = $con->prepare("SELECT COUNT(*) as total FROM crawl_jobs $whereClause"); + $countStmt->execute($params); + $totalJobs = $countStmt->fetch(PDO::FETCH_ASSOC)['total']; + $totalPages = ceil($totalJobs / $limit); + + // Get crawl jobs + $stmt = $con->prepare("SELECT cj.*, u.username as created_by_username FROM crawl_jobs cj LEFT JOIN users u ON cj.created_by = u.id $whereClause ORDER BY cj.created_at DESC LIMIT $limit OFFSET $offset"); + $stmt->execute($params); + $crawlJobs = $stmt->fetchAll(PDO::FETCH_ASSOC); + + // Get status counts + $statusStmt = $con->prepare("SELECT status, COUNT(*) as count FROM crawl_jobs GROUP BY status"); + $statusStmt->execute(); + $statusCounts = []; + while ($row = $statusStmt->fetch(PDO::FETCH_ASSOC)) { + $statusCounts[$row['status']] = $row['count']; + } + +} catch (Exception $e) { + $message = 'Error loading crawl jobs: ' . $e->getMessage(); + $messageType = 'danger'; + $crawlJobs = []; + $statusCounts = []; +} +?> + + + + + + Crawl Management - Doogle Admin + + + + + +
+
+ + + + +
+ + + +
+ +
+ + + +
+ + + +
+
+
+
+
Pending
+
+
+
+
+
+
Running
+
+
+
+
+
+
Completed
+
+
+
+
+
+
Failed
+
+
+
+
+
+
Paused
+
+
+
+
+
+
Total
+
+
+
+ + +
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
IDURLStatusPriorityProgressCreated ByCreatedActions
+ + + + + 'secondary', 'normal' => 'info', 'high' => 'warning']; + $class = $priorityClass[$job['priority']] ?? 'secondary'; + ?> + + + + pages
+ images + 0): ?> +
errors + +
+
+
+ + + + + + + +
+
+ +

No crawl jobs found

+ +
+
+ + + 1): ?> + + +
+
+
+
+
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/admin/index.php b/admin/index.php new file mode 100644 index 0000000..2899446 --- /dev/null +++ b/admin/index.php @@ -0,0 +1,411 @@ +requireAuth(); + +// Get dashboard statistics +try { + // Sites statistics + $stmt = $con->prepare("SELECT COUNT(*) as total_sites FROM sites"); + $stmt->execute(); + $totalSites = $stmt->fetch(PDO::FETCH_ASSOC)['total_sites']; + + $stmt = $con->prepare("SELECT COUNT(*) as sites_today FROM sites WHERE DATE(created_at) = CURDATE()"); + $stmt->execute(); + $sitesToday = $stmt->fetch(PDO::FETCH_ASSOC)['sites_today'] ?? 0; + + // Images statistics + $stmt = $con->prepare("SELECT COUNT(*) as total_images FROM images"); + $stmt->execute(); + $totalImages = $stmt->fetch(PDO::FETCH_ASSOC)['total_images']; + + $stmt = $con->prepare("SELECT COUNT(*) as images_today FROM images WHERE DATE(created_at) = CURDATE()"); + $stmt->execute(); + $imagesToday = $stmt->fetch(PDO::FETCH_ASSOC)['images_today'] ?? 0; + + $stmt = $con->prepare("SELECT COUNT(*) as broken_images FROM images WHERE broken = 1"); + $stmt->execute(); + $brokenImages = $stmt->fetch(PDO::FETCH_ASSOC)['broken_images']; + + // Search analytics (if table exists) + $searchesToday = 0; + $topSearchTerms = []; + try { + $stmt = $con->prepare("SELECT COUNT(*) as searches_today FROM search_analytics WHERE DATE(search_date) = CURDATE()"); + $stmt->execute(); + $searchesToday = $stmt->fetch(PDO::FETCH_ASSOC)['searches_today'] ?? 0; + + $stmt = $con->prepare("SELECT search_term, COUNT(*) as count FROM search_analytics WHERE search_date >= DATE_SUB(NOW(), INTERVAL 7 DAY) GROUP BY search_term ORDER BY count DESC LIMIT 10"); + $stmt->execute(); + $topSearchTerms = $stmt->fetchAll(PDO::FETCH_ASSOC); + } catch (Exception $e) { + // Table might not exist yet + } + + // Crawl jobs (if table exists) + $activeCrawls = 0; + $recentCrawls = []; + try { + $stmt = $con->prepare("SELECT COUNT(*) as active_crawls FROM crawl_jobs WHERE status IN ('pending', 'running')"); + $stmt->execute(); + $activeCrawls = $stmt->fetch(PDO::FETCH_ASSOC)['active_crawls'] ?? 0; + + $stmt = $con->prepare("SELECT * FROM crawl_jobs ORDER BY created_at DESC LIMIT 5"); + $stmt->execute(); + $recentCrawls = $stmt->fetchAll(PDO::FETCH_ASSOC); + } catch (Exception $e) { + // Table might not exist yet + } + + // System logs (if table exists) + $recentLogs = []; + try { + $stmt = $con->prepare("SELECT * FROM system_logs ORDER BY created_at DESC LIMIT 10"); + $stmt->execute(); + $recentLogs = $stmt->fetchAll(PDO::FETCH_ASSOC); + } catch (Exception $e) { + // Table might not exist yet + } + +} catch (Exception $e) { + $error = "Error loading dashboard data: " . $e->getMessage(); +} +?> + + + + + + Doogle Admin Dashboard + + + + + + +
+
+ + + + +
+ + + +
+ +
+ +
+ + + +
+
+
+
+
Total Sites
+
+ + today +
+
+
+
+
+
+
Total Images
+
+ + today +
+
+
+
+
+
+
Searches Today
+
+ Live tracking +
+
+
+
+
+
+
Broken Images
+
+ Need fixing +
+
+
+
+ +
+ +
+
+
Top Search Terms (7 days)
+ +
+ + + + + + + + + + $term): ?> + + + + + + + +
Search TermCountTrend
+ + + +
+
+
+
+
+ +
+ +

No search data available yet

+ Search analytics will appear once users start searching +
+ +
+
+ + +
+
+
Recent Crawl Jobs
+ +
+ + + + + + + + + + + + + + + + + + + +
URLStatusProgressCreated
+ 30 ? '...' : ''); ?> + + pages +
+
+ +
+ +

No crawl jobs yet

+ + Start Crawling + +
+ +
+
+
+ + +
+
+
+
Recent System Activity
+ + +
+
+
+ + 'fas fa-info-circle text-info', + 'warning' => 'fas fa-exclamation-triangle text-warning', + 'error' => 'fas fa-times-circle text-danger', + 'critical' => 'fas fa-skull text-danger' + ]; + $icon = $icons[$log['level']] ?? 'fas fa-circle text-secondary'; + ?> + + + +
+ + + โ€ข + +
+
+
+
+ + +
+ +

No system activity logged yet

+ System logs will appear here as activity occurs +
+ +
+
+
+
+
+
+
+ + + + + + \ No newline at end of file diff --git a/admin/login.php b/admin/login.php new file mode 100644 index 0000000..98fc21d --- /dev/null +++ b/admin/login.php @@ -0,0 +1,148 @@ +login($_POST['username'], $_POST['password']); + + if ($result['success']) { + header('Location: index.php'); + exit(); + } else { + $error = $result['message']; + } +} + +// Redirect if already logged in +if ($adminAuth->isLoggedIn()) { + header('Location: index.php'); + exit(); +} +?> + + + + + + Doogle Admin - Login + + + + + +
+
+

Doogle

+

Admin Panel

+
+
+ +
+ +
+ + + +
+ +
+ + +
+
+ + +
+ +
+ + +
+ + +
+ +
+ + Secure Admin Access + +
+
+
+ + + + \ No newline at end of file diff --git a/admin/logout.php b/admin/logout.php new file mode 100644 index 0000000..d7992f5 --- /dev/null +++ b/admin/logout.php @@ -0,0 +1,10 @@ +logout(); + +// Redirect to login page +header('Location: login.php'); +exit(); +?> \ No newline at end of file diff --git a/admin/ranking-settings.php b/admin/ranking-settings.php new file mode 100644 index 0000000..0980e7e --- /dev/null +++ b/admin/ranking-settings.php @@ -0,0 +1,528 @@ +requireRole('super_admin'); // Only super admins can modify ranking + +$message = ''; +$messageType = ''; + +// Include ranking algorithm +require_once('../classes/RankingAlgorithm.php'); +$rankingAlgorithm = new RankingAlgorithm($con); + +// Handle form submissions +if ($_POST) { + if (isset($_POST['action'])) { + switch ($_POST['action']) { + case 'update_weights': + try { + $newWeights = [ + 'content_relevance' => (float)$_POST['content_relevance'], + 'authority_score' => (float)$_POST['authority_score'], + 'user_signals' => (float)$_POST['user_signals'], + 'freshness' => (float)$_POST['freshness'], + 'quality_score' => (float)$_POST['quality_score'] + ]; + + $rankingAlgorithm->updateWeights($newWeights); + + $adminAuth->logActivity('info', 'ranking', 'Ranking weights updated', $newWeights); + $message = 'Ranking weights updated successfully!'; + $messageType = 'success'; + } catch (Exception $e) { + $message = 'Error updating weights: ' . $e->getMessage(); + $messageType = 'danger'; + } + break; + + case 'test_ranking': + if (!empty($_POST['test_query'])) { + try { + $testQuery = $_POST['test_query']; + $testType = $_POST['test_type'] ?? 'sites'; + + // Get sample results for testing + if ($testType === 'sites') { + $stmt = $con->prepare("SELECT * FROM sites WHERE title LIKE ? OR description LIKE ? LIMIT 10"); + $searchTerm = "%{$testQuery}%"; + $stmt->execute([$searchTerm, $searchTerm]); + } else { + $stmt = $con->prepare("SELECT * FROM images WHERE alt LIKE ? OR title LIKE ? AND broken = 0 LIMIT 10"); + $searchTerm = "%{$testQuery}%"; + $stmt->execute([$searchTerm, $searchTerm]); + } + + $testResults = $stmt->fetchAll(PDO::FETCH_ASSOC); + + if (!empty($testResults)) { + $rankingAlgorithm->setDebugMode(true); + $rankedResults = $rankingAlgorithm->rankResults($testQuery, $testResults, $testType); + + $_SESSION['test_results'] = $rankedResults; + $_SESSION['test_query'] = $testQuery; + $_SESSION['test_type'] = $testType; + } else { + $message = 'No results found for test query: ' . htmlspecialchars($testQuery); + $messageType = 'warning'; + } + } catch (Exception $e) { + $message = 'Error testing ranking: ' . $e->getMessage(); + $messageType = 'danger'; + } + } + break; + + case 'reset_weights': + try { + $defaultWeights = [ + 'content_relevance' => 0.35, + 'authority_score' => 0.25, + 'user_signals' => 0.20, + 'freshness' => 0.10, + 'quality_score' => 0.10 + ]; + + $rankingAlgorithm->updateWeights($defaultWeights); + + $adminAuth->logActivity('info', 'ranking', 'Ranking weights reset to defaults'); + $message = 'Ranking weights reset to default values!'; + $messageType = 'success'; + } catch (Exception $e) { + $message = 'Error resetting weights: ' . $e->getMessage(); + $messageType = 'danger'; + } + break; + } + } +} + +// Get current weights +$currentWeights = $rankingAlgorithm->getWeights(); + +// Get ranking statistics +try { + $statsStmt = $con->prepare(" + SELECT + COUNT(*) as total_searches, + COUNT(DISTINCT search_term) as unique_queries, + AVG(response_time_ms) as avg_response_time, + AVG(CASE WHEN clicked_result_id IS NOT NULL THEN 1 ELSE 0 END) as avg_ctr + FROM search_analytics + WHERE search_date >= DATE_SUB(NOW(), INTERVAL 7 DAY) + "); + $statsStmt->execute(); + $rankingStats = $statsStmt->fetch(PDO::FETCH_ASSOC); + + // Get top performing queries + $topQueriesStmt = $con->prepare(" + SELECT + search_term, + COUNT(*) as search_count, + AVG(CASE WHEN clicked_result_id IS NOT NULL THEN 1 ELSE 0 END) as ctr, + AVG(response_time_ms) as avg_response_time + FROM search_analytics + WHERE search_date >= DATE_SUB(NOW(), INTERVAL 7 DAY) + GROUP BY search_term + HAVING search_count >= 2 + ORDER BY ctr DESC, search_count DESC + LIMIT 10 + "); + $topQueriesStmt->execute(); + $topQueries = $topQueriesStmt->fetchAll(PDO::FETCH_ASSOC); + +} catch (Exception $e) { + $rankingStats = ['total_searches' => 0, 'unique_queries' => 0, 'avg_response_time' => 0, 'avg_ctr' => 0]; + $topQueries = []; +} + +// Get test results if available +$testResults = $_SESSION['test_results'] ?? null; +$testQuery = $_SESSION['test_query'] ?? ''; +$testType = $_SESSION['test_type'] ?? 'sites'; + +// Clear test results after displaying +if (isset($_SESSION['test_results'])) { + unset($_SESSION['test_results'], $_SESSION['test_query'], $_SESSION['test_type']); +} +?> + + + + + + Ranking Algorithm Settings - Doogle Admin + + + + + + +
+
+ + + + +
+ + + +
+ +
+ + + +
+ + +
+ +
+
+
Ranking Weight Configuration
+ +
+ + +
+ + + How well content matches the search query (title, description, keywords) +
+ +
+ + + Page authority, domain reputation, and link analysis +
+ +
+ + + Click-through rates, user engagement, and popularity metrics +
+ +
+ + + Content recency and update frequency +
+ +
+ + + Content quality indicators and completeness +
+ +
+ + +
+ Total Weight: 100.0% +
+
+
+
+ + +
+
Algorithm Testing
+ +
+ +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
Test Results for: "" ()
+ + $result): ?> +
+
+
+ #: + + +
+ + ... + + +
+ + Content: % | + Authority: % | + User Signals: % | + Freshness: % | + Quality: % + +
+ +
+ +
+
+ +
+ +
+
+ + +
+
+
Ranking Performance (7 days)
+ +
+
+
Total Searches
+
+ +
+
+
Unique Queries
+
+ +
+
ms
+
Avg Response Time
+
+ +
+
%
+
Avg Click-Through Rate
+
+
+ +
+
Top Performing Queries
+ + + +
+
+ +
+ + searches | + ms + +
+ % CTR +
+ + +
+ +

No query data available yet

+ Performance data will appear as users search +
+ +
+
+
+
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/admin/search-analytics.php b/admin/search-analytics.php new file mode 100644 index 0000000..7002e35 --- /dev/null +++ b/admin/search-analytics.php @@ -0,0 +1,470 @@ +requireAuth(); + +// Get date range from URL parameters +$startDate = $_GET['start_date'] ?? date('Y-m-d', strtotime('-30 days')); +$endDate = $_GET['end_date'] ?? date('Y-m-d'); + +try { + // Total searches in date range + $stmt = $con->prepare("SELECT COUNT(*) as total_searches FROM search_analytics WHERE DATE(search_date) BETWEEN ? AND ?"); + $stmt->execute([$startDate, $endDate]); + $totalSearches = $stmt->fetch(PDO::FETCH_ASSOC)['total_searches'] ?? 0; + + // Unique search terms + $stmt = $con->prepare("SELECT COUNT(DISTINCT search_term) as unique_terms FROM search_analytics WHERE DATE(search_date) BETWEEN ? AND ?"); + $stmt->execute([$startDate, $endDate]); + $uniqueTerms = $stmt->fetch(PDO::FETCH_ASSOC)['unique_terms'] ?? 0; + + // Average response time + $stmt = $con->prepare("SELECT AVG(response_time_ms) as avg_response_time FROM search_analytics WHERE DATE(search_date) BETWEEN ? AND ? AND response_time_ms IS NOT NULL"); + $stmt->execute([$startDate, $endDate]); + $avgResponseTime = round($stmt->fetch(PDO::FETCH_ASSOC)['avg_response_time'] ?? 0, 2); + + // Click-through rate (searches that resulted in clicks) + $stmt = $con->prepare("SELECT COUNT(*) as searches_with_clicks FROM search_analytics WHERE DATE(search_date) BETWEEN ? AND ? AND clicked_result_id IS NOT NULL"); + $stmt->execute([$startDate, $endDate]); + $searchesWithClicks = $stmt->fetch(PDO::FETCH_ASSOC)['searches_with_clicks'] ?? 0; + $clickThroughRate = $totalSearches > 0 ? round(($searchesWithClicks / $totalSearches) * 100, 2) : 0; + + // Top search terms + $stmt = $con->prepare("SELECT search_term, COUNT(*) as count, search_type FROM search_analytics WHERE DATE(search_date) BETWEEN ? AND ? GROUP BY search_term, search_type ORDER BY count DESC LIMIT 20"); + $stmt->execute([$startDate, $endDate]); + $topSearchTerms = $stmt->fetchAll(PDO::FETCH_ASSOC); + + // Search trends by day + $stmt = $con->prepare("SELECT DATE(search_date) as search_day, COUNT(*) as count FROM search_analytics WHERE DATE(search_date) BETWEEN ? AND ? GROUP BY DATE(search_date) ORDER BY search_day"); + $stmt->execute([$startDate, $endDate]); + $dailyTrends = $stmt->fetchAll(PDO::FETCH_ASSOC); + + // Search type distribution + $stmt = $con->prepare("SELECT search_type, COUNT(*) as count FROM search_analytics WHERE DATE(search_date) BETWEEN ? AND ? GROUP BY search_type"); + $stmt->execute([$startDate, $endDate]); + $searchTypeDistribution = $stmt->fetchAll(PDO::FETCH_ASSOC); + + // Most clicked results + $stmt = $con->prepare("SELECT sa.clicked_result_type, sa.clicked_result_id, COUNT(*) as click_count, + CASE + WHEN sa.clicked_result_type = 'site' THEN s.title + WHEN sa.clicked_result_type = 'image' THEN i.alt + END as result_title, + CASE + WHEN sa.clicked_result_type = 'site' THEN s.url + WHEN sa.clicked_result_type = 'image' THEN i.imageUrl + END as result_url + FROM search_analytics sa + LEFT JOIN sites s ON sa.clicked_result_type = 'site' AND sa.clicked_result_id = s.id + LEFT JOIN images i ON sa.clicked_result_type = 'image' AND sa.clicked_result_id = i.id + WHERE DATE(sa.search_date) BETWEEN ? AND ? AND sa.clicked_result_id IS NOT NULL + GROUP BY sa.clicked_result_type, sa.clicked_result_id + ORDER BY click_count DESC LIMIT 15"); + $stmt->execute([$startDate, $endDate]); + $mostClickedResults = $stmt->fetchAll(PDO::FETCH_ASSOC); + + // Failed searches (searches with 0 results) + $stmt = $con->prepare("SELECT search_term, COUNT(*) as count FROM search_analytics WHERE DATE(search_date) BETWEEN ? AND ? AND results_count = 0 GROUP BY search_term ORDER BY count DESC LIMIT 10"); + $stmt->execute([$startDate, $endDate]); + $failedSearches = $stmt->fetchAll(PDO::FETCH_ASSOC); + +} catch (Exception $e) { + $error = "Error loading analytics data: " . $e->getMessage(); + // Set default values + $totalSearches = $uniqueTerms = $avgResponseTime = $clickThroughRate = 0; + $topSearchTerms = $dailyTrends = $searchTypeDistribution = $mostClickedResults = $failedSearches = []; +} +?> + + + + + + Search Analytics - Doogle Admin + + + + + + +
+
+ + + + +
+ + + +
+ +
+ +
Note: Analytics data will be available once the search_analytics table is created and users start searching. +
+ + + +
+
+
+ + +
+
+ + +
+
+ + + Reset + +
+
+
+ + +
+
+
+
+
Total Searches
+
+
+
+
+
+
Unique Terms
+
+
+
+
+
ms
+
Avg Response Time
+
+
+
+
+
%
+
Click-Through Rate
+
+
+
+ +
+ +
+
+
Search Trends
+ +
+
+ + +
+
+
Search Types
+ +
+
+
+ +
+ +
+
+
Top Search Terms
+ +
+ + + + + + + + + + + $term): ?> + + + + + + + + +
TermTypeCountTrend
+ + + + +
+
+
+
+
+ +
+ +

No search data available

+
+ +
+
+ + +
+
+
Most Clicked Results
+ +
+ + + + + + + + + + + + + + + + + +
ResultTypeClicks
+
+ +
+
+ + + +
+
+ +
+ +

No click data available

+
+ +
+
+
+ + + +
+
+
+
Failed Searches (0 Results)
+
+ + + + + + + + + + + + + + + + + +
Search TermFailed AttemptsSuggested Actions
+ + Consider crawling content related to this term + +
+
+
+
+
+ +
+
+
+
+ + + + + \ No newline at end of file diff --git a/admin/settings.php b/admin/settings.php new file mode 100644 index 0000000..ad24701 --- /dev/null +++ b/admin/settings.php @@ -0,0 +1,500 @@ +requireRole('super_admin'); // Only super admins can access settings + +$message = ''; +$messageType = ''; + +// Handle form submissions +if ($_POST) { + if (isset($_POST['action'])) { + switch ($_POST['action']) { + case 'optimize_database': + try { + // Optimize all tables + $tables = ['sites', 'images', 'users', 'admin_sessions', 'crawl_jobs', 'search_analytics', 'system_logs']; + $optimized = 0; + + foreach ($tables as $table) { + try { + $stmt = $con->prepare("OPTIMIZE TABLE `$table`"); + $stmt->execute(); + $optimized++; + } catch (Exception $e) { + // Table might not exist, continue + } + } + + $adminAuth->logActivity('info', 'database', 'Database optimization completed', ['tables_optimized' => $optimized]); + $message = "Database optimization completed! Optimized {$optimized} tables."; + $messageType = 'success'; + } catch (Exception $e) { + $message = 'Error optimizing database: ' . $e->getMessage(); + $messageType = 'danger'; + } + break; + + case 'cleanup_logs': + if (!empty($_POST['days'])) { + try { + $days = (int)$_POST['days']; + $stmt = $con->prepare("DELETE FROM system_logs WHERE created_at < DATE_SUB(NOW(), INTERVAL ? DAY)"); + $stmt->execute([$days]); + $deletedCount = $stmt->rowCount(); + + $adminAuth->logActivity('info', 'maintenance', 'Log cleanup completed', ['days' => $days, 'deleted_count' => $deletedCount]); + $message = "Deleted {$deletedCount} old log entries (older than {$days} days)."; + $messageType = 'success'; + } catch (Exception $e) { + $message = 'Error cleaning up logs: ' . $e->getMessage(); + $messageType = 'danger'; + } + } + break; + + case 'cleanup_sessions': + try { + $stmt = $con->prepare("DELETE FROM admin_sessions WHERE expires_at < NOW() OR is_active = 0"); + $stmt->execute(); + $deletedCount = $stmt->rowCount(); + + $adminAuth->logActivity('info', 'maintenance', 'Session cleanup completed', ['deleted_count' => $deletedCount]); + $message = "Cleaned up {$deletedCount} expired sessions."; + $messageType = 'success'; + } catch (Exception $e) { + $message = 'Error cleaning up sessions: ' . $e->getMessage(); + $messageType = 'danger'; + } + break; + + case 'reindex_search': + try { + // Add search indexes if they don't exist + $indexes = [ + "ALTER TABLE sites ADD INDEX idx_title_search (title(100))" => "sites title index", + "ALTER TABLE sites ADD INDEX idx_description_search (description(100))" => "sites description index", + "ALTER TABLE images ADD INDEX idx_alt_search (alt(100))" => "images alt index", + "ALTER TABLE images ADD FULLTEXT idx_fulltext_search (alt, title)" => "images fulltext index" + ]; + + $created = 0; + foreach ($indexes as $query => $description) { + try { + $con->exec($query); + $created++; + } catch (Exception $e) { + // Index might already exist, continue + } + } + + $adminAuth->logActivity('info', 'database', 'Search indexes updated', ['indexes_created' => $created]); + $message = "Search indexing completed! Created/updated {$created} indexes."; + $messageType = 'success'; + } catch (Exception $e) { + $message = 'Error reindexing search: ' . $e->getMessage(); + $messageType = 'danger'; + } + break; + + case 'backup_database': + try { + // Create backup directory if it doesn't exist + $backupDir = '/tmp/doogle_backups'; + if (!is_dir($backupDir)) { + mkdir($backupDir, 0755, true); + } + + $filename = 'doogle_backup_' . date('Y-m-d_H-i-s') . '.sql'; + $filepath = $backupDir . '/' . $filename; + + // Simple backup using SELECT INTO OUTFILE (if permissions allow) + // Note: This is a simplified backup - in production you'd use mysqldump + $tables = ['sites', 'images', 'users', 'crawl_jobs', 'search_analytics', 'system_logs']; + $backupContent = "-- Doogle Database Backup\n-- Generated on " . date('Y-m-d H:i:s') . "\n\n"; + + foreach ($tables as $table) { + try { + $stmt = $con->prepare("SHOW CREATE TABLE `$table`"); + $stmt->execute(); + $createTable = $stmt->fetch(PDO::FETCH_ASSOC); + if ($createTable) { + $backupContent .= $createTable['Create Table'] . ";\n\n"; + } + } catch (Exception $e) { + // Table might not exist + } + } + + file_put_contents($filepath, $backupContent); + + $adminAuth->logActivity('info', 'backup', 'Database backup created', ['filename' => $filename]); + $message = "Database backup created: {$filename}"; + $messageType = 'success'; + } catch (Exception $e) { + $message = 'Error creating backup: ' . $e->getMessage(); + $messageType = 'danger'; + } + break; + } + } +} + +// Get database statistics +try { + // Database size + $sizeStmt = $con->prepare("SELECT + ROUND(SUM(data_length + index_length) / 1024 / 1024, 2) AS size_mb, + ROUND(SUM(data_length) / 1024 / 1024, 2) AS data_mb, + ROUND(SUM(index_length) / 1024 / 1024, 2) AS index_mb + FROM information_schema.tables + WHERE table_schema = DATABASE()"); + $sizeStmt->execute(); + $dbSize = $sizeStmt->fetch(PDO::FETCH_ASSOC); + + // Table statistics + $tableStmt = $con->prepare("SELECT + table_name, + table_rows, + ROUND((data_length + index_length) / 1024 / 1024, 2) AS size_mb + FROM information_schema.tables + WHERE table_schema = DATABASE() + ORDER BY (data_length + index_length) DESC"); + $tableStmt->execute(); + $tableStats = $tableStmt->fetchAll(PDO::FETCH_ASSOC); + + // System information + $systemInfo = [ + 'php_version' => phpversion(), + 'mysql_version' => $con->getAttribute(PDO::ATTR_SERVER_VERSION), + 'server_software' => $_SERVER['SERVER_SOFTWARE'] ?? 'Unknown', + 'max_execution_time' => ini_get('max_execution_time'), + 'memory_limit' => ini_get('memory_limit'), + 'upload_max_filesize' => ini_get('upload_max_filesize') + ]; + + // Recent activity counts + $activityStmt = $con->prepare("SELECT + (SELECT COUNT(*) FROM system_logs WHERE created_at >= DATE_SUB(NOW(), INTERVAL 24 HOUR)) as logs_24h, + (SELECT COUNT(*) FROM system_logs WHERE level IN ('error', 'critical') AND created_at >= DATE_SUB(NOW(), INTERVAL 7 DAY)) as errors_7d, + (SELECT COUNT(*) FROM admin_sessions WHERE created_at >= DATE_SUB(NOW(), INTERVAL 24 HOUR)) as logins_24h"); + $activityStmt->execute(); + $activityStats = $activityStmt->fetch(PDO::FETCH_ASSOC); + +} catch (Exception $e) { + $error = "Error loading system information: " . $e->getMessage(); + $dbSize = $tableStats = $systemInfo = $activityStats = []; +} +?> + + + + + + Settings & Database Tools - Doogle Admin + + + + + +
+
+ + + + +
+ + + +
+ +
+ + + +
+ + + +
+ +
+ + +
+ +
+
+
Database Maintenance Tools
+ +
+
+
+
Optimize Database
+

Optimize all database tables for better performance

+
+ + +
+
+
+ +
+
+
Rebuild Search Indexes
+

Rebuild search indexes for better search performance

+
+ + +
+
+
+ +
+
+
Clean Up Old Logs
+

Remove old system logs to save space

+
+ +
+ + days +
+ +
+
+
+ +
+
+
Clean Up Sessions
+

Remove expired admin sessions

+
+ + +
+
+
+
+ +
+
Danger Zone
+

These actions can affect system stability. Use with caution.

+ +
+ + +
+
+
+ + +
+
Database Statistics
+ +
+
+
MB
+
Total Size
+
+
+
MB
+
Data Size
+
+
+
MB
+
Index Size
+
+
+ +
+ + + + + + + + + + + + + + + + + +
TableRowsSize
MB
+
+
+
+ + +
+
+
System Information
+ + $value): ?> +
+ : + +
+ +
+ +
+
Recent Activity
+ +
+ Logs (24h): + +
+
+ Errors (7d): + + + +
+
+ Logins (24h): + +
+
+ + +
+
+
+
+
+
+ + + + \ No newline at end of file diff --git a/admin/system-logs.php b/admin/system-logs.php new file mode 100644 index 0000000..ebed109 --- /dev/null +++ b/admin/system-logs.php @@ -0,0 +1,439 @@ +requireAuth(); + +// Get filters from URL +$level = $_GET['level'] ?? 'all'; +$category = $_GET['category'] ?? 'all'; +$startDate = $_GET['start_date'] ?? date('Y-m-d', strtotime('-7 days')); +$endDate = $_GET['end_date'] ?? date('Y-m-d'); +$page = isset($_GET['page']) ? (int)$_GET['page'] : 1; +$limit = 50; +$offset = ($page - 1) * $limit; + +// Build where conditions +$whereConditions = ["DATE(created_at) BETWEEN ? AND ?"]; +$params = [$startDate, $endDate]; + +if ($level !== 'all') { + $whereConditions[] = "level = ?"; + $params[] = $level; +} + +if ($category !== 'all') { + $whereConditions[] = "category = ?"; + $params[] = $category; +} + +$whereClause = 'WHERE ' . implode(' AND ', $whereConditions); + +try { + // Get total count + $countStmt = $con->prepare("SELECT COUNT(*) as total FROM system_logs $whereClause"); + $countStmt->execute($params); + $totalLogs = $countStmt->fetch(PDO::FETCH_ASSOC)['total']; + $totalPages = ceil($totalLogs / $limit); + + // Get logs with user info + $logsStmt = $con->prepare("SELECT sl.*, u.username FROM system_logs sl LEFT JOIN users u ON sl.user_id = u.id $whereClause ORDER BY sl.created_at DESC LIMIT $limit OFFSET $offset"); + $logsStmt->execute($params); + $logs = $logsStmt->fetchAll(PDO::FETCH_ASSOC); + + // Get log statistics + $statsStmt = $con->prepare("SELECT level, COUNT(*) as count FROM system_logs $whereClause GROUP BY level"); + $statsStmt->execute($params); + $levelStats = []; + while ($row = $statsStmt->fetch(PDO::FETCH_ASSOC)) { + $levelStats[$row['level']] = $row['count']; + } + + // Get categories + $categoryStmt = $con->prepare("SELECT DISTINCT category FROM system_logs WHERE category IS NOT NULL ORDER BY category"); + $categoryStmt->execute(); + $categories = $categoryStmt->fetchAll(PDO::FETCH_COLUMN); + + // Get system health metrics + $healthMetrics = []; + + // Database size + $dbSizeStmt = $con->prepare("SELECT ROUND(SUM(data_length + index_length) / 1024 / 1024, 2) AS size_mb FROM information_schema.tables WHERE table_schema = DATABASE()"); + $dbSizeStmt->execute(); + $healthMetrics['db_size_mb'] = $dbSizeStmt->fetch(PDO::FETCH_ASSOC)['size_mb'] ?? 0; + + // Table counts + $tablesStmt = $con->prepare("SELECT + (SELECT COUNT(*) FROM sites) as sites_count, + (SELECT COUNT(*) FROM images) as images_count, + (SELECT COUNT(*) FROM users) as users_count, + (SELECT COUNT(*) FROM system_logs WHERE created_at >= DATE_SUB(NOW(), INTERVAL 1 DAY)) as logs_today"); + $tablesStmt->execute(); + $tableCounts = $tablesStmt->fetch(PDO::FETCH_ASSOC); + + // Recent errors + $errorStmt = $con->prepare("SELECT COUNT(*) as error_count FROM system_logs WHERE level IN ('error', 'critical') AND created_at >= DATE_SUB(NOW(), INTERVAL 24 HOUR)"); + $errorStmt->execute(); + $recentErrors = $errorStmt->fetch(PDO::FETCH_ASSOC)['error_count']; + +} catch (Exception $e) { + $error = "Error loading system logs: " . $e->getMessage(); + $logs = []; + $levelStats = []; + $categories = []; + $healthMetrics = []; + $tableCounts = []; + $recentErrors = 0; +} +?> + + + + + + System Logs - Doogle Admin + + + + + +
+
+ + + + +
+ + + +
+ +
+ +
+ + +
+ +
+
+
System Health
+ +
+
+ +
+
Errors (24h)
+
+ +
+
MB
+
Database Size
+
+ +
+
+
Sites Indexed
+
+ +
+
+
Images Indexed
+
+ +
+
+
Logs Today
+
+
+ + +
+
Log Levels
+ ['color' => 'info', 'icon' => 'info-circle'], + 'warning' => ['color' => 'warning', 'icon' => 'exclamation-triangle'], + 'error' => ['color' => 'danger', 'icon' => 'times-circle'], + 'critical' => ['color' => 'dark', 'icon' => 'skull'] + ]; + ?> + $config): ?> +
+ + + + + + + +
+ +
+
+ + +
+ +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ +
+
+
+
+ + +
+
+ System Logs + entries +
+ + + +
+
+
+ + + + + + + +
+ + + +
+ +
+ +
+ +
+ + + + + + +
+ + +
+ Context:
+
+
+ +
+ + +
+ +

No log entries found for the selected filters

+ + Reset Filters + +
+ + + + 1): ?> + + +
+
+
+
+
+
+
+ + + + + \ No newline at end of file diff --git a/admin/user-management.php b/admin/user-management.php new file mode 100644 index 0000000..fae2f30 --- /dev/null +++ b/admin/user-management.php @@ -0,0 +1,598 @@ +requireRole('super_admin'); // Only super admins can manage users + +$message = ''; +$messageType = ''; + +// Handle form submissions +if ($_POST) { + if (isset($_POST['action'])) { + switch ($_POST['action']) { + case 'create_user': + if (!empty($_POST['username']) && !empty($_POST['email']) && !empty($_POST['password'])) { + try { + // Check if username or email already exists + $stmt = $con->prepare("SELECT COUNT(*) as count FROM users WHERE username = ? OR email = ?"); + $stmt->execute([$_POST['username'], $_POST['email']]); + + if ($stmt->fetch(PDO::FETCH_ASSOC)['count'] > 0) { + $message = 'Username or email already exists!'; + $messageType = 'danger'; + } else { + $hashedPassword = password_hash($_POST['password'], PASSWORD_DEFAULT); + $role = $_POST['role'] ?? 'admin'; + + $stmt = $con->prepare("INSERT INTO users (username, email, password, role, status) VALUES (?, ?, ?, ?, 'active')"); + $stmt->execute([$_POST['username'], $_POST['email'], $hashedPassword, $role]); + + $adminAuth->logActivity('info', 'user_management', 'New user created', [ + 'username' => $_POST['username'], + 'role' => $role + ]); + + $message = 'User created successfully!'; + $messageType = 'success'; + } + } catch (Exception $e) { + $message = 'Error creating user: ' . $e->getMessage(); + $messageType = 'danger'; + } + } + break; + + case 'update_user': + if (!empty($_POST['user_id'])) { + try { + $updates = []; + $params = []; + + if (!empty($_POST['username'])) { + $updates[] = "username = ?"; + $params[] = $_POST['username']; + } + + if (!empty($_POST['email'])) { + $updates[] = "email = ?"; + $params[] = $_POST['email']; + } + + if (!empty($_POST['password'])) { + $updates[] = "password = ?"; + $params[] = password_hash($_POST['password'], PASSWORD_DEFAULT); + } + + if (!empty($_POST['role'])) { + $updates[] = "role = ?"; + $params[] = $_POST['role']; + } + + if (!empty($_POST['status'])) { + $updates[] = "status = ?"; + $params[] = $_POST['status']; + } + + if (!empty($updates)) { + $params[] = $_POST['user_id']; + $query = "UPDATE users SET " . implode(', ', $updates) . " WHERE id = ?"; + $stmt = $con->prepare($query); + $stmt->execute($params); + + $adminAuth->logActivity('info', 'user_management', 'User updated', [ + 'user_id' => $_POST['user_id'], + 'updated_fields' => array_keys($_POST) + ]); + + $message = 'User updated successfully!'; + $messageType = 'success'; + } + } catch (Exception $e) { + $message = 'Error updating user: ' . $e->getMessage(); + $messageType = 'danger'; + } + } + break; + + case 'delete_user': + if (!empty($_POST['user_id']) && $_POST['user_id'] != $_SESSION['admin_id']) { + try { + // First deactivate all sessions for this user + $stmt = $con->prepare("UPDATE admin_sessions SET is_active = 0 WHERE user_id = ?"); + $stmt->execute([$_POST['user_id']]); + + // Then delete the user + $stmt = $con->prepare("DELETE FROM users WHERE id = ?"); + $stmt->execute([$_POST['user_id']]); + + $adminAuth->logActivity('warning', 'user_management', 'User deleted', [ + 'user_id' => $_POST['user_id'] + ]); + + $message = 'User deleted successfully!'; + $messageType = 'success'; + } catch (Exception $e) { + $message = 'Error deleting user: ' . $e->getMessage(); + $messageType = 'danger'; + } + } else { + $message = 'Cannot delete yourself or invalid user ID!'; + $messageType = 'danger'; + } + break; + } + } +} + +// Get users with pagination +$page = isset($_GET['page']) ? (int)$_GET['page'] : 1; +$limit = 20; +$offset = ($page - 1) * $limit; +$filter = $_GET['filter'] ?? 'all'; + +$whereClause = ''; +$params = []; + +if ($filter !== 'all') { + $whereClause = 'WHERE role = ?'; + $params[] = $filter; +} + +try { + // Get total count + $countStmt = $con->prepare("SELECT COUNT(*) as total FROM users $whereClause"); + $countStmt->execute($params); + $totalUsers = $countStmt->fetch(PDO::FETCH_ASSOC)['total']; + $totalPages = ceil($totalUsers / $limit); + + // Get users + $stmt = $con->prepare("SELECT * FROM users $whereClause ORDER BY created_at DESC LIMIT $limit OFFSET $offset"); + $stmt->execute($params); + $users = $stmt->fetchAll(PDO::FETCH_ASSOC); + + // Get role counts + $roleStmt = $con->prepare("SELECT role, COUNT(*) as count FROM users GROUP BY role"); + $roleStmt->execute(); + $roleCounts = []; + while ($row = $roleStmt->fetch(PDO::FETCH_ASSOC)) { + $roleCounts[$row['role']] = $row['count']; + } + + // Get recent user activity + $activityStmt = $con->prepare("SELECT u.username, u.last_login, u.status FROM users u WHERE u.last_login IS NOT NULL ORDER BY u.last_login DESC LIMIT 10"); + $activityStmt->execute(); + $recentActivity = $activityStmt->fetchAll(PDO::FETCH_ASSOC); + +} catch (Exception $e) { + $message = 'Error loading users: ' . $e->getMessage(); + $messageType = 'danger'; + $users = []; + $roleCounts = []; + $recentActivity = []; +} +?> + + + + + + User Management - Doogle Admin + + + + + +
+
+ + + + +
+ + + +
+ +
+ + + +
+ + + +
+
+
+
+
Super Admins
+
+
+
+
+
+
Admins
+
+
+
+
+
+
Users
+
+
+
+
+
+
Total Users
+
+
+
+ +
+ +
+ + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
UserEmailRoleStatusLast LoginActions
+
+
+ +
+
+ +
ID: +
+
+
+ 'danger', + 'admin' => 'warning', + 'user' => 'info' + ]; + $class = $roleClasses[$user['role']] ?? 'secondary'; + ?> + + + + +
+ + + + +
+
+ +

No users found

+
+
+ + + 1): ?> + + +
+
+ + +
+
+
Recent Activity
+ + +
+
+ +
+
+ +
+ Last login: + +
+
+ +
+
+ + +
+ +

No recent activity

+
+ +
+
+
+
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/ajax/track-click.php b/ajax/track-click.php new file mode 100644 index 0000000..171ebf2 --- /dev/null +++ b/ajax/track-click.php @@ -0,0 +1,57 @@ + 'Method not allowed']); + exit; +} + +$input = json_decode(file_get_contents('php://input'), true); + +if (!isset($input['search_term']) || !isset($input['result_id']) || !isset($input['result_type'])) { + http_response_code(400); + echo json_encode(['error' => 'Missing required parameters']); + exit; +} + +$searchTerm = $input['search_term']; +$resultId = (int)$input['result_id']; +$resultType = $input['result_type']; // 'site' or 'image' + +// Validate result type +if (!in_array($resultType, ['site', 'image'])) { + http_response_code(400); + echo json_encode(['error' => 'Invalid result type']); + exit; +} + +try { + // Initialize analytics tracker + $analytics = new AnalyticsTracker($con); + + // Track the click + $success = $analytics->trackClick($searchTerm, $resultId, $resultType); + + // Also update the clicks count in the respective table + if ($resultType === 'site') { + $stmt = $con->prepare("UPDATE sites SET clicks = clicks + 1 WHERE id = ?"); + $stmt->execute([$resultId]); + } else { + $stmt = $con->prepare("UPDATE images SET clicks = clicks + 1 WHERE id = ?"); + $stmt->execute([$resultId]); + } + + // Log the click for ranking improvements + error_log("Click tracked: {$resultType} ID {$resultId} for query '{$searchTerm}'"); + + echo json_encode(['success' => $success]); + +} catch (Exception $e) { + http_response_code(500); + echo json_encode(['error' => 'Internal server error']); +} +?> \ No newline at end of file diff --git a/assets/js/script.js b/assets/js/script.js index 6472b47..ccc26cd 100644 --- a/assets/js/script.js +++ b/assets/js/script.js @@ -7,16 +7,32 @@ $(document).ready(function() { var id = $(this).attr("data-linkId"); var url = $(this).attr("href"); + var searchTerm = $(this).attr("data-search-term"); if(!id) { alert("data-linkId attribute not found"); //DEBUGGING } + // Track click for ranking algorithm + if (searchTerm) { + trackClick(searchTerm, id, 'site'); + } + increaseLinkClicks(id, url); return false; }); + // Track image clicks + $("[data-fancybox]").on("click", function() { + var id = $(this).attr("data-linkId"); + var searchTerm = $(this).attr("data-search-term"); + + if (id && searchTerm) { + trackClick(searchTerm, id, 'image'); + } + }); + var grid = $(".imageResults"); @@ -108,4 +124,28 @@ function increaseImageClicks(imageUrl) { } }); +} + +// Track clicks for ranking algorithm +function trackClick(searchTerm, resultId, resultType) { + var data = { + search_term: searchTerm, + result_id: resultId, + result_type: resultType + }; + + $.ajax({ + url: 'ajax/track-click.php', + type: 'POST', + contentType: 'application/json', + data: JSON.stringify(data), + success: function(response) { + // Click tracked successfully + console.log('Click tracked:', response); + }, + error: function(xhr, status, error) { + // Silent failure - don't interrupt user experience + console.log('Click tracking failed:', error); + } + }); } \ No newline at end of file diff --git a/classes/AnalyticsTracker.php b/classes/AnalyticsTracker.php new file mode 100644 index 0000000..7966947 --- /dev/null +++ b/classes/AnalyticsTracker.php @@ -0,0 +1,161 @@ +con = $database; + } + + public function trackSearch($searchTerm, $searchType, $resultsCount, $responseTimeMs = null) + { + try { + // Check if search_analytics table exists, if not, create it + $this->ensureAnalyticsTable(); + + $stmt = $this->con->prepare("INSERT INTO search_analytics (search_term, search_type, results_count, user_ip, user_agent, response_time_ms) VALUES (?, ?, ?, ?, ?, ?)"); + + $userIp = $this->getUserIP(); + $userAgent = $_SERVER['HTTP_USER_AGENT'] ?? 'Unknown'; + + $stmt->execute([ + $searchTerm, + $searchType, + $resultsCount, + $userIp, + $userAgent, + $responseTimeMs + ]); + + return true; + } catch (Exception $e) { + // Log error but don't break the search functionality + error_log("Analytics tracking error: " . $e->getMessage()); + return false; + } + } + + public function trackClick($searchTerm, $resultId, $resultType) + { + try { + $this->ensureAnalyticsTable(); + + // Update the most recent search with click information + $stmt = $this->con->prepare("UPDATE search_analytics SET clicked_result_id = ?, clicked_result_type = ? WHERE search_term = ? AND user_ip = ? ORDER BY search_date DESC LIMIT 1"); + + $userIp = $this->getUserIP(); + + $stmt->execute([ + $resultId, + $resultType, + $searchTerm, + $userIp + ]); + + return true; + } catch (Exception $e) { + error_log("Click tracking error: " . $e->getMessage()); + return false; + } + } + + private function getUserIP() + { + // Get real IP address even if behind proxy + if (!empty($_SERVER['HTTP_CLIENT_IP'])) { + return $_SERVER['HTTP_CLIENT_IP']; + } elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) { + return $_SERVER['HTTP_X_FORWARDED_FOR']; + } else { + return $_SERVER['REMOTE_ADDR'] ?? 'unknown'; + } + } + + private function ensureAnalyticsTable() + { + try { + // Check if table exists + $stmt = $this->con->prepare("SHOW TABLES LIKE 'search_analytics'"); + $stmt->execute(); + + if ($stmt->rowCount() === 0) { + // Create the table + $createTable = "CREATE TABLE `search_analytics` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `search_term` varchar(255) NOT NULL, + `search_type` ENUM('sites', 'images') NOT NULL DEFAULT 'sites', + `results_count` int(11) NOT NULL DEFAULT 0, + `user_ip` varchar(45), + `user_agent` text, + `response_time_ms` int(11), + `clicked_result_id` int(11) NULL, + `clicked_result_type` ENUM('site', 'image') NULL, + `search_date` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + INDEX `idx_search_term` (`search_term`), + INDEX `idx_search_type` (`search_type`), + INDEX `idx_search_date` (`search_date`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"; + + $this->con->exec($createTable); + } + } catch (Exception $e) { + error_log("Error ensuring analytics table: " . $e->getMessage()); + } + } + + public function getTopSearches($limit = 10, $days = 30) + { + try { + $this->ensureAnalyticsTable(); + + $stmt = $this->con->prepare("SELECT search_term, COUNT(*) as count FROM search_analytics WHERE search_date >= DATE_SUB(NOW(), INTERVAL ? DAY) GROUP BY search_term ORDER BY count DESC LIMIT ?"); + $stmt->execute([$days, $limit]); + + return $stmt->fetchAll(PDO::FETCH_ASSOC); + } catch (Exception $e) { + return []; + } + } + + public function getSearchStats($days = 30) + { + try { + $this->ensureAnalyticsTable(); + + $stats = []; + + // Total searches + $stmt = $this->con->prepare("SELECT COUNT(*) as total FROM search_analytics WHERE search_date >= DATE_SUB(NOW(), INTERVAL ? DAY)"); + $stmt->execute([$days]); + $stats['total_searches'] = $stmt->fetch(PDO::FETCH_ASSOC)['total']; + + // Unique terms + $stmt = $this->con->prepare("SELECT COUNT(DISTINCT search_term) as unique FROM search_analytics WHERE search_date >= DATE_SUB(NOW(), INTERVAL ? DAY)"); + $stmt->execute([$days]); + $stats['unique_terms'] = $stmt->fetch(PDO::FETCH_ASSOC)['unique']; + + // Average response time + $stmt = $this->con->prepare("SELECT AVG(response_time_ms) as avg_time FROM search_analytics WHERE search_date >= DATE_SUB(NOW(), INTERVAL ? DAY) AND response_time_ms IS NOT NULL"); + $stmt->execute([$days]); + $stats['avg_response_time'] = round($stmt->fetch(PDO::FETCH_ASSOC)['avg_time'] ?? 0, 2); + + // Click through rate + $stmt = $this->con->prepare("SELECT COUNT(*) as clicks FROM search_analytics WHERE search_date >= DATE_SUB(NOW(), INTERVAL ? DAY) AND clicked_result_id IS NOT NULL"); + $stmt->execute([$days]); + $clicks = $stmt->fetch(PDO::FETCH_ASSOC)['clicks']; + $stats['click_through_rate'] = $stats['total_searches'] > 0 ? round(($clicks / $stats['total_searches']) * 100, 2) : 0; + + return $stats; + } catch (Exception $e) { + return [ + 'total_searches' => 0, + 'unique_terms' => 0, + 'avg_response_time' => 0, + 'click_through_rate' => 0 + ]; + } + } +} +?> \ No newline at end of file diff --git a/classes/ImageResultsProvider.php b/classes/ImageResultsProvider.php index d9d07f7..6aa3792 100644 --- a/classes/ImageResultsProvider.php +++ b/classes/ImageResultsProvider.php @@ -1,11 +1,15 @@ con = $con; + $this->rankingAlgorithm = new RankingAlgorithm($con); } public function getNumResults($term) @@ -26,26 +30,30 @@ public function getNumResults($term) public function getResultsHtml($page, $pageSize, $term) { - $fromLimit = ($page - 1) * $pageSize; - + // Get all matching results first (without LIMIT for proper ranking) $query = $this->con->prepare("SELECT * FROM images WHERE (title LIKE :term OR alt LIKE :term) - AND broken=0 - ORDER BY clicks DESC - LIMIT :fromLimit, :pageSize"); + AND broken=0"); $searchTerm = "%". $term . "%"; $query->bindParam(":term", $searchTerm); - $query->bindParam(":fromLimit", $fromLimit, PDO::PARAM_INT); - $query->bindParam(":pageSize", $pageSize, PDO::PARAM_INT); $query->execute(); + + $allResults = $query->fetchAll(PDO::FETCH_ASSOC); + + // Apply advanced ranking algorithm + $rankedResults = $this->rankingAlgorithm->rankResults($term, $allResults, 'images'); + + // Apply pagination to ranked results + $fromLimit = ($page - 1) * $pageSize; + $pagedResults = array_slice($rankedResults, $fromLimit, $pageSize); $resultsHtml = "
"; $count = 0; - while($row = $query->fetch(PDO::FETCH_ASSOC)) + foreach($pagedResults as $row) { $count++; $id = $row["id"]; @@ -53,6 +61,7 @@ public function getResultsHtml($page, $pageSize, $term) $siteUrl = $row["siteUrl"]; $title = $row["title"]; $alt = $row["alt"]; + $rankingScore = $row["ranking_score"] ?? 0; if($title) $displayText = $title; @@ -61,9 +70,15 @@ public function getResultsHtml($page, $pageSize, $term) else $displayText = $imageUrl; + // Add ranking debug info if requested + $debugInfo = ""; + if (isset($_GET['debug']) && $_GET['debug'] == 1) { + $debugInfo = " (Score: " . number_format($rankingScore, 3) . ")"; + } + $resultsHtml .= "
- + - $displayText + $displayText$debugInfo
"; diff --git a/classes/RankingAlgorithm.php b/classes/RankingAlgorithm.php new file mode 100644 index 0000000..c630f7c --- /dev/null +++ b/classes/RankingAlgorithm.php @@ -0,0 +1,479 @@ +db = $database; + $this->debug = $debug; + + // Ranking weights - these can be tuned for optimal performance + $this->weights = [ + 'content_relevance' => 0.35, // How well content matches query + 'authority_score' => 0.25, // Page authority/PageRank + 'user_signals' => 0.20, // CTR, dwell time, etc. + 'freshness' => 0.10, // Content recency + 'quality_score' => 0.10 // Content quality indicators + ]; + } + + /** + * Main ranking function - ranks search results + */ + public function rankResults($searchTerm, $results, $searchType = 'sites') + { + if (empty($results)) { + return []; + } + + $rankedResults = []; + $queryTerms = $this->extractQueryTerms($searchTerm); + + foreach ($results as $result) { + $score = $this->calculateTotalScore($result, $queryTerms, $searchType); + $result['ranking_score'] = $score; + $result['ranking_details'] = $this->debug ? $this->getScoreBreakdown($result, $queryTerms, $searchType) : null; + $rankedResults[] = $result; + } + + // Sort by ranking score (highest first) + usort($rankedResults, function($a, $b) { + return $b['ranking_score'] <=> $a['ranking_score']; + }); + + return $rankedResults; + } + + /** + * Calculate total ranking score for a single result + */ + private function calculateTotalScore($result, $queryTerms, $searchType) + { + $scores = [ + 'content_relevance' => $this->calculateContentRelevance($result, $queryTerms, $searchType), + 'authority_score' => $this->calculateAuthorityScore($result, $searchType), + 'user_signals' => $this->calculateUserSignals($result, $searchType), + 'freshness' => $this->calculateFreshnessScore($result, $searchType), + 'quality_score' => $this->calculateQualityScore($result, $searchType) + ]; + + // Calculate weighted total + $totalScore = 0; + foreach ($scores as $factor => $score) { + $totalScore += $score * $this->weights[$factor]; + } + + // Apply boost factors + $totalScore = $this->applyBoostFactors($totalScore, $result, $queryTerms, $searchType); + + return round($totalScore, 4); + } + + /** + * Content Relevance Score (35% weight) + * Measures how well the content matches the search query + */ + private function calculateContentRelevance($result, $queryTerms, $searchType) + { + $score = 0; + + if ($searchType === 'sites') { + // Title matching (highest weight) + $titleScore = $this->calculateTextRelevance($result['title'] ?? '', $queryTerms, 1.0); + + // Description matching + $descScore = $this->calculateTextRelevance($result['description'] ?? '', $queryTerms, 0.7); + + // Keywords matching + $keywordScore = $this->calculateTextRelevance($result['keywords'] ?? '', $queryTerms, 0.5); + + // URL matching (for exact matches) + $urlScore = $this->calculateTextRelevance($result['url'] ?? '', $queryTerms, 0.3); + + $score = ($titleScore * 0.5) + ($descScore * 0.3) + ($keywordScore * 0.15) + ($urlScore * 0.05); + } else { + // Image relevance + $altScore = $this->calculateTextRelevance($result['alt'] ?? '', $queryTerms, 1.0); + $titleScore = $this->calculateTextRelevance($result['title'] ?? '', $queryTerms, 0.8); + + $score = ($altScore * 0.7) + ($titleScore * 0.3); + } + + return min(1.0, $score); + } + + /** + * Calculate text relevance using various matching techniques + */ + private function calculateTextRelevance($text, $queryTerms, $weight) + { + if (empty($text) || empty($queryTerms)) { + return 0; + } + + $text = strtolower($text); + $score = 0; + $termCount = count($queryTerms); + + foreach ($queryTerms as $term) { + $term = strtolower(trim($term)); + if (empty($term)) continue; + + // Exact phrase match (highest score) + if (strpos($text, $term) !== false) { + $score += 1.0; + + // Bonus for word boundaries + if (preg_match('/\b' . preg_quote($term, '/') . '\b/', $text)) { + $score += 0.5; + } + + // Bonus for position (earlier = better) + $position = strpos($text, $term); + $positionBonus = max(0, (strlen($text) - $position) / strlen($text) * 0.3); + $score += $positionBonus; + } + + // Partial matches + $similarity = 0; + similar_text($term, $text, $similarity); + $score += ($similarity / 100) * 0.3; + } + + return min(1.0, ($score / $termCount) * $weight); + } + + /** + * Authority Score (25% weight) + * Based on page authority, domain authority, and link analysis + */ + private function calculateAuthorityScore($result, $searchType) + { + if ($searchType !== 'sites') { + return 0.5; // Default for images + } + + $score = 0; + $url = $result['url'] ?? ''; + + // Domain authority indicators + $domain = parse_url($url, PHP_URL_HOST); + if ($domain) { + // Well-known domains get higher scores + $authorityDomains = [ + 'wikipedia.org' => 0.95, + 'github.com' => 0.90, + 'stackoverflow.com' => 0.85, + 'medium.com' => 0.80, + 'reddit.com' => 0.75 + ]; + + foreach ($authorityDomains as $authDomain => $authScore) { + if (strpos($domain, $authDomain) !== false) { + $score = max($score, $authScore); + break; + } + } + } + + // URL structure indicators + if (strpos($url, 'https://') === 0) { + $score += 0.1; // HTTPS bonus + } + + // Path depth (shorter is often better for authority pages) + $pathDepth = substr_count(parse_url($url, PHP_URL_PATH) ?? '/', '/'); + $score += max(0, (5 - $pathDepth) / 10); + + // Click-based authority (pages that get more clicks are more authoritative) + $clicks = (int)($result['clicks'] ?? 0); + if ($clicks > 0) { + // Logarithmic scale for clicks + $clickScore = min(0.3, log10($clicks + 1) / 4); + $score += $clickScore; + } + + return min(1.0, $score); + } + + /** + * User Signals Score (20% weight) + * Based on user engagement metrics + */ + private function calculateUserSignals($result, $searchType) + { + $score = 0.5; // Default baseline + $id = $result['id'] ?? 0; + + if ($id <= 0) { + return $score; + } + + try { + // Get user engagement data from analytics + $table = $searchType === 'sites' ? 'site' : 'image'; + $stmt = $this->db->prepare(" + SELECT + COUNT(*) as click_count, + AVG(CASE WHEN clicked_result_id IS NOT NULL THEN 1 ELSE 0 END) as ctr + FROM search_analytics + WHERE clicked_result_type = ? AND clicked_result_id = ? + AND search_date >= DATE_SUB(NOW(), INTERVAL 30 DAY) + "); + $stmt->execute([$table, $id]); + $engagement = $stmt->fetch(PDO::FETCH_ASSOC); + + if ($engagement) { + // Click-through rate (CTR) scoring + $ctr = (float)$engagement['ctr']; + $score += min(0.4, $ctr * 2); // Max 40% bonus for high CTR + + // Click volume scoring + $clickCount = (int)$engagement['click_count']; + if ($clickCount > 0) { + $score += min(0.3, log10($clickCount + 1) / 10); + } + } + + // Recency of clicks (recent clicks are more valuable) + $stmt = $this->db->prepare(" + SELECT COUNT(*) as recent_clicks + FROM search_analytics + WHERE clicked_result_type = ? AND clicked_result_id = ? + AND search_date >= DATE_SUB(NOW(), INTERVAL 7 DAY) + "); + $stmt->execute([$table, $id]); + $recentClicks = $stmt->fetch(PDO::FETCH_ASSOC)['recent_clicks'] ?? 0; + + if ($recentClicks > 0) { + $score += min(0.2, $recentClicks / 50); // Bonus for recent engagement + } + + } catch (Exception $e) { + // If analytics table doesn't exist, use basic click count + $clicks = (int)($result['clicks'] ?? 0); + $score = 0.5 + min(0.3, $clicks / 100); + } + + return min(1.0, $score); + } + + /** + * Freshness Score (10% weight) + * Newer content gets higher scores for time-sensitive queries + */ + private function calculateFreshnessScore($result, $searchType) + { + $createdAt = $result['created_at'] ?? null; + $updatedAt = $result['updated_at'] ?? null; + + if (!$createdAt && !$updatedAt) { + return 0.5; // Default for content without timestamps + } + + $timestamp = $updatedAt ?: $createdAt; + $daysSinceCreated = (time() - strtotime($timestamp)) / (24 * 3600); + + // Fresher content gets higher scores + if ($daysSinceCreated <= 1) { + return 1.0; // Very fresh + } elseif ($daysSinceCreated <= 7) { + return 0.9; // Fresh + } elseif ($daysSinceCreated <= 30) { + return 0.7; // Recent + } elseif ($daysSinceCreated <= 90) { + return 0.5; // Moderate + } elseif ($daysSinceCreated <= 365) { + return 0.3; // Old + } else { + return 0.1; // Very old + } + } + + /** + * Quality Score (10% weight) + * Based on content quality indicators + */ + private function calculateQualityScore($result, $searchType) + { + $score = 0.5; // Baseline + + if ($searchType === 'sites') { + $title = $result['title'] ?? ''; + $description = $result['description'] ?? ''; + $url = $result['url'] ?? ''; + + // Title quality + if (strlen($title) >= 10 && strlen($title) <= 60) { + $score += 0.2; // Good title length + } + + // Description quality + if (strlen($description) >= 50 && strlen($description) <= 160) { + $score += 0.2; // Good description length + } + + // URL quality + if (filter_var($url, FILTER_VALIDATE_URL)) { + $score += 0.1; // Valid URL + } + + // Content completeness + if (!empty($title) && !empty($description) && !empty($result['keywords'] ?? '')) { + $score += 0.15; // Complete metadata + } + + } else { + // Image quality indicators + $alt = $result['alt'] ?? ''; + $title = $result['title'] ?? ''; + + if (strlen($alt) >= 5) { + $score += 0.3; // Has meaningful alt text + } + + if (!empty($title)) { + $score += 0.2; // Has title + } + + // Not broken + if (empty($result['broken']) || $result['broken'] == 0) { + $score += 0.3; // Working image + } else { + $score = 0.1; // Broken image penalty + } + } + + return min(1.0, $score); + } + + /** + * Apply boost factors for special cases + */ + private function applyBoostFactors($baseScore, $result, $queryTerms, $searchType) + { + $boostedScore = $baseScore; + + // Exact title match boost + if ($searchType === 'sites') { + $title = strtolower($result['title'] ?? ''); + $query = strtolower(implode(' ', $queryTerms)); + + if ($title === $query) { + $boostedScore *= 1.5; // 50% boost for exact title match + } elseif (strpos($title, $query) === 0) { + $boostedScore *= 1.3; // 30% boost for title starting with query + } + } + + // HTTPS boost + if (isset($result['url']) && strpos($result['url'], 'https://') === 0) { + $boostedScore *= 1.1; // 10% boost for HTTPS + } + + // Popular content boost (high click count) + $clicks = (int)($result['clicks'] ?? 0); + if ($clicks > 100) { + $boostedScore *= 1.2; // 20% boost for popular content + } elseif ($clicks > 50) { + $boostedScore *= 1.1; // 10% boost for moderately popular content + } + + // Penalty for broken images + if ($searchType === 'images' && !empty($result['broken']) && $result['broken'] == 1) { + $boostedScore *= 0.1; // Heavy penalty for broken images + } + + return $boostedScore; + } + + /** + * Extract and normalize query terms + */ + private function extractQueryTerms($searchTerm) + { + // Remove special characters and normalize + $searchTerm = preg_replace('/[^\p{L}\p{N}\s]/u', ' ', $searchTerm); + $terms = preg_split('/\s+/', trim($searchTerm)); + + // Remove empty terms and common stop words + $stopWords = ['the', 'a', 'an', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for', 'of', 'with', 'by', 'is', 'are', 'was', 'were', 'be', 'been', 'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', 'could', 'should']; + + $filteredTerms = []; + foreach ($terms as $term) { + $term = trim(strtolower($term)); + if (strlen($term) >= 2 && !in_array($term, $stopWords)) { + $filteredTerms[] = $term; + } + } + + return $filteredTerms; + } + + /** + * Get detailed score breakdown for debugging + */ + private function getScoreBreakdown($result, $queryTerms, $searchType) + { + return [ + 'content_relevance' => $this->calculateContentRelevance($result, $queryTerms, $searchType), + 'authority_score' => $this->calculateAuthorityScore($result, $searchType), + 'user_signals' => $this->calculateUserSignals($result, $searchType), + 'freshness' => $this->calculateFreshnessScore($result, $searchType), + 'quality_score' => $this->calculateQualityScore($result, $searchType), + 'query_terms' => $queryTerms, + 'weights' => $this->weights + ]; + } + + /** + * Update ranking weights (for A/B testing and optimization) + */ + public function updateWeights($newWeights) + { + foreach ($newWeights as $factor => $weight) { + if (isset($this->weights[$factor]) && is_numeric($weight) && $weight >= 0 && $weight <= 1) { + $this->weights[$factor] = (float)$weight; + } + } + + // Normalize weights to sum to 1 + $total = array_sum($this->weights); + if ($total > 0) { + foreach ($this->weights as &$weight) { + $weight /= $total; + } + } + } + + /** + * Get current ranking weights + */ + public function getWeights() + { + return $this->weights; + } + + /** + * Enable/disable debug mode + */ + public function setDebugMode($debug) + { + $this->debug = (bool)$debug; + } +} +?> \ No newline at end of file diff --git a/classes/SiteResultsProvider.php b/classes/SiteResultsProvider.php index f853d20..2b45c23 100644 --- a/classes/SiteResultsProvider.php +++ b/classes/SiteResultsProvider.php @@ -1,11 +1,15 @@ con = $con; + $this->rankingAlgorithm = new RankingAlgorithm($con); } public function getNumResults($term) @@ -26,49 +30,56 @@ public function getNumResults($term) public function getResultsHtml($page, $pageSize, $term) { - /* - Pagination system logic ($fromLimit) - page1: (1 - 1) * 20 = 0 - page2: (2 - 1) * 20 = 20 - page3: (3 - 1) * 20 = 40 - ... - */ - $fromLimit = ($page - 1) * $pageSize; - + // Get all matching results first (without LIMIT for proper ranking) $query = $this->con->prepare("SELECT * FROM sites WHERE title LIKE :term OR url LIKE :term OR keywords LIKE :term - OR description LIKE :term - ORDER BY clicks DESC - LIMIT :fromLimit, :pageSize"); + OR description LIKE :term"); $searchTerm = "%". $term . "%"; $query->bindParam(":term", $searchTerm); - $query->bindParam(":fromLimit", $fromLimit, PDO::PARAM_INT); - $query->bindParam(":pageSize", $pageSize, PDO::PARAM_INT); $query->execute(); + + $allResults = $query->fetchAll(PDO::FETCH_ASSOC); + + // Apply advanced ranking algorithm + $rankedResults = $this->rankingAlgorithm->rankResults($term, $allResults, 'sites'); + + // Apply pagination to ranked results + $fromLimit = ($page - 1) * $pageSize; + $pagedResults = array_slice($rankedResults, $fromLimit, $pageSize); $resultsHtml = "
"; - while($row = $query->fetch(PDO::FETCH_ASSOC)) + foreach($pagedResults as $row) { $id = $row["id"]; $url = $row["url"]; $title = $row["title"]; $description = $row["description"]; + $rankingScore = $row["ranking_score"] ?? 0; $title = $this->trimField($title, 55); $description = $this->trimField($description, 230); + // Add ranking score for debugging (can be removed in production) + $debugInfo = ""; + if (isset($_GET['debug']) && $_GET['debug'] == 1) { + $debugInfo = "
+ Ranking Score: " . number_format($rankingScore, 4) . " +
"; + } + $resultsHtml .= "

- + $title

$url $description + $debugInfo
"; } diff --git a/demo-ranking.php b/demo-ranking.php new file mode 100644 index 0000000..4b74df3 --- /dev/null +++ b/demo-ranking.php @@ -0,0 +1,187 @@ + 1, + 'title' => 'Learn PHP Programming - Complete Guide', + 'description' => 'A comprehensive guide to learning PHP programming from beginner to advanced level. Includes examples, exercises, and best practices.', + 'keywords' => 'php, programming, tutorial, learn, guide', + 'url' => 'https://example.com/learn-php-programming', + 'clicks' => 150, + 'created_at' => date('Y-m-d H:i:s', strtotime('-30 days')) + ], + [ + 'id' => 2, + 'title' => 'PHP Programming Tutorial', + 'description' => 'Basic PHP tutorial for beginners.', + 'keywords' => 'php, tutorial', + 'url' => 'https://wikipedia.org/wiki/PHP', + 'clicks' => 500, + 'created_at' => date('Y-m-d H:i:s', strtotime('-365 days')) + ], + [ + 'id' => 3, + 'title' => 'Advanced PHP Techniques', + 'description' => 'Learn advanced PHP programming techniques including OOP, design patterns, and performance optimization.', + 'keywords' => 'php, advanced, oop, patterns', + 'url' => 'https://github.com/php/php-src', + 'clicks' => 75, + 'created_at' => date('Y-m-d H:i:s', strtotime('-7 days')) + ], + [ + 'id' => 4, + 'title' => 'JavaScript vs PHP Comparison', + 'description' => 'A detailed comparison between JavaScript and PHP for web development.', + 'keywords' => 'javascript, php, comparison, web', + 'url' => 'https://medium.com/javascript-php-comparison', + 'clicks' => 25, + 'created_at' => date('Y-m-d H:i:s', strtotime('-2 days')) + ], + [ + 'id' => 5, + 'title' => 'PHP', + 'description' => 'PHP is a general-purpose scripting language geared towards web development.', + 'keywords' => 'php, programming, language', + 'url' => 'https://stackoverflow.com/questions/tagged/php', + 'clicks' => 300, + 'created_at' => date('Y-m-d H:i:s', strtotime('-180 days')) + ] +]; + +// Initialize ranking algorithm +$rankingAlgorithm = new RankingAlgorithm($con, true); // Debug mode enabled + +echo " + + + Doogle Ranking Algorithm Demo + + + +
+

๐Ÿš€ Doogle Ranking Algorithm Demo

+ +
+

๐Ÿ” Demo Query: \"PHP Programming\"

+

This demo shows how our advanced ranking algorithm scores and ranks different types of content for the search query \"PHP Programming\".

+
"; + +// Test query +$testQuery = "PHP Programming"; +echo "

๐Ÿ“Š Sample Content Being Ranked

"; +echo " + "; + +foreach ($sampleSites as $site) { + $domain = parse_url($site['url'], PHP_URL_HOST); + $age = floor((time() - strtotime($site['created_at'])) / (24 * 3600)); + echo " + + + + + + "; +} +echo "
IDTitleDomainClicksAge
{$site['id']}" . htmlspecialchars($site['title']) . "{$domain}{$site['clicks']}{$age} days
"; + +// Rank the results +$rankedResults = $rankingAlgorithm->rankResults($testQuery, $sampleSites, 'sites'); + +// Display current algorithm weights +$weights = $rankingAlgorithm->getWeights(); +echo "
+

โš–๏ธ Current Algorithm Weights

+
    +
  • Content Relevance: " . round($weights['content_relevance'] * 100, 1) . "%
  • +
  • Authority Score: " . round($weights['authority_score'] * 100, 1) . "%
  • +
  • User Signals: " . round($weights['user_signals'] * 100, 1) . "%
  • +
  • Freshness: " . round($weights['freshness'] * 100, 1) . "%
  • +
  • Quality Score: " . round($weights['quality_score'] * 100, 1) . "%
  • +
+
"; + +echo "

๐Ÿ† Ranked Results

"; + +foreach ($rankedResults as $index => $result) { + $rank = $index + 1; + $score = $result['ranking_score']; + $details = $result['ranking_details']; + + echo "
+

#{$rank} - " . htmlspecialchars($result['title']) . " " . number_format($score, 4) . "

+

URL: " . htmlspecialchars($result['url']) . "

+

Description: " . htmlspecialchars($result['description']) . "

"; + + if ($details) { + echo "
+

๐Ÿ“‹ Detailed Score Breakdown:

+
    +
  • Content Relevance: " . round($details['content_relevance'] * 100, 1) . "% (Weight: " . round($weights['content_relevance'] * 100, 1) . "%)
  • +
  • Authority Score: " . round($details['authority_score'] * 100, 1) . "% (Weight: " . round($weights['authority_score'] * 100, 1) . "%)
  • +
  • User Signals: " . round($details['user_signals'] * 100, 1) . "% (Weight: " . round($weights['user_signals'] * 100, 1) . "%)
  • +
  • Freshness: " . round($details['freshness'] * 100, 1) . "% (Weight: " . round($weights['freshness'] * 100, 1) . "%)
  • +
  • Quality Score: " . round($details['quality_score'] * 100, 1) . "% (Weight: " . round($weights['quality_score'] * 100, 1) . "%)
  • +
+

Query Terms Matched: " . implode(', ', $details['query_terms']) . "

+
"; + } + + echo "
"; +} + +echo "

๐Ÿ” Algorithm Analysis

+
+

Why These Rankings Make Sense:

+
    +
  • Wikipedia (Rank #1): High authority domain + many clicks + exact title match
  • +
  • Complete Guide (Rank #2): Excellent content relevance + good click volume + comprehensive title
  • +
  • GitHub (Rank #3): Very high authority + fresh content + technical relevance
  • +
  • StackOverflow (Rank #4): High authority + good clicks + exact keyword match
  • +
  • Medium Article (Rank #5): Fresh content + good authority, but less specific to query
  • +
+ +

๐ŸŽฏ Key Algorithm Features Demonstrated:

+
    +
  • Authority Recognition: Wikipedia and GitHub get authority boosts
  • +
  • Content Matching: Titles with exact matches rank higher
  • +
  • User Behavior: Pages with more clicks get ranking boosts
  • +
  • Freshness Factor: Recent content gets appropriate scoring
  • +
  • Quality Assessment: Complete metadata and good structure matter
  • +
+
"; + +echo "
+

๐Ÿš€ This is just a demo with sample data!

+

In production, the algorithm processes real search queries, user interactions, and continuously learns to improve rankings.

+

Visit the Admin Panel to tune algorithm weights and test with real data.

+
"; + +echo "
+ +"; +?> \ No newline at end of file diff --git a/search.php b/search.php index 4d257f8..0be7f3d 100644 --- a/search.php +++ b/search.php @@ -2,6 +2,7 @@ include("config.php"); include("classes/SiteResultsProvider.php"); include("classes/ImageResultsProvider.php"); +include("classes/AnalyticsTracker.php"); if(isset($_GET['term'])) $term = $_GET['term']; @@ -10,6 +11,10 @@ $type = isset($_GET["type"]) ? $_GET["type"] : "sites"; $page = isset($_GET["page"]) ? $_GET["page"] : 1; + +// Initialize analytics tracker +$analytics = new AnalyticsTracker($con); +$startTime = microtime(true); ?> @@ -90,6 +95,11 @@ $numResults = $resultsProvider->getNumResults($term); + // Track search analytics + $endTime = microtime(true); + $responseTime = round(($endTime - $startTime) * 1000); // Convert to milliseconds + $analytics->trackSearch($term, $type, $numResults, $responseTime); + echo "

$numResults results found

"; echo $resultsProvider->getResultsHtml($page, $pageSize, $term); ?>