meili_host = getenv('MEILI_HOST') ?: 'http://meilisearch:7700'; $this->meili_key = getenv('MEILI_MASTER_KEY') ?: 'changeme_meilisearch_master_key'; $this->db = new VDatabase(); $this->logger = new VLogger('search_indexer'); } /** * Initialize Meilisearch index with settings */ public function initializeIndex() { $this->logger->info("Initializing Meilisearch index"); // Create/update index settings $settings = [ 'searchableAttributes' => [ 'title', 'description', 'tags', 'category', 'username' ], 'filterableAttributes' => [ 'category', 'content_type', 'duration_range', 'upload_date', 'is_live', 'privacy' ], 'sortableAttributes' => [ 'upload_date', 'view_count', 'like_count', 'duration' ], 'rankingRules' => [ 'words', 'typo', 'proximity', 'attribute', 'sort', 'exactness' ], 'stopWords' => ['the', 'a', 'an'], 'synonyms' => [ 'video' => ['clip', 'recording'], 'live' => ['stream', 'broadcast'] ] ]; $response = $this->apiRequest('PUT', '/indexes/videos/settings', $settings); if ($response) { $this->logger->info("Index settings updated successfully"); return true; } $this->logger->error("Failed to update index settings"); return false; } /** * Index all videos (full reindex) */ public function indexAllVideos($batch_size = 100) { $this->logger->info("Starting full video reindex"); $offset = 0; $total_indexed = 0; while (true) { $videos = $this->fetchVideos($offset, $batch_size); if (empty($videos)) { break; } $documents = array_map([$this, 'transformVideoToDocument'], $videos); if ($this->indexDocuments($documents)) { $total_indexed += count($documents); $this->logger->info("Indexed batch: " . count($documents) . " videos (Total: $total_indexed)"); } else { $this->logger->error("Failed to index batch at offset $offset"); } $offset += $batch_size; } $this->logger->info("Full reindex complete. Total videos indexed: $total_indexed"); return $total_indexed; } /** * Index single video */ public function indexVideo($video_id) { $video = $this->fetchVideoById($video_id); if (!$video) { $this->logger->warning("Video not found: $video_id"); return false; } $document = $this->transformVideoToDocument($video); return $this->indexDocuments([$document]); } /** * Remove video from index */ public function removeVideo($video_id) { $response = $this->apiRequest('DELETE', "/indexes/videos/documents/$video_id"); if ($response) { $this->logger->info("Removed video from index: $video_id"); return true; } return false; } /** * Fetch videos from database */ private function fetchVideos($offset, $limit) { $sql = "SELECT v.video_id, v.video_title as title, v.video_description as description, v.video_tags as tags, v.video_category as category, v.video_duration as duration, v.video_views as view_count, v.video_liked as like_count, v.upload_date, v.approved, v.privacy, u.usr_user as username, u.usr_id as user_id, 'video' as content_type, 0 as is_live FROM db_videofiles v LEFT JOIN db_accountuser u ON v.usr_id = u.usr_id WHERE v.approved = 1 AND v.privacy != 'private' ORDER BY v.upload_date DESC LIMIT $limit OFFSET $offset"; $result = $this->db->execute($sql); if (!$result) { return []; } return $this->db->resultsToArray($result); } /** * Fetch single video by ID */ private function fetchVideoById($video_id) { $sql = "SELECT v.video_id, v.video_title as title, v.video_description as description, v.video_tags as tags, v.video_category as category, v.video_duration as duration, v.video_views as view_count, v.video_liked as like_count, v.upload_date, v.approved, v.privacy, u.usr_user as username, u.usr_id as user_id, 'video' as content_type, 0 as is_live FROM db_videofiles v LEFT JOIN db_accountuser u ON v.usr_id = u.usr_id WHERE v.video_id = " . intval($video_id); $result = $this->db->execute($sql); if (!$result || $this->db->rowCount($result) == 0) { return null; } return $this->db->fetch($result); } /** * Transform video data to Meilisearch document */ private function transformVideoToDocument($video) { // Determine duration range $duration = intval($video['duration']); if ($duration < 60) { $duration_range = 'under_1min'; } elseif ($duration < 300) { $duration_range = '1_5min'; } elseif ($duration < 600) { $duration_range = '5_10min'; } elseif ($duration < 1200) { $duration_range = '10_20min'; } else { $duration_range = 'over_20min'; } return [ 'id' => 'video_' . $video['video_id'], 'video_id' => intval($video['video_id']), 'title' => $video['title'] ?? '', 'description' => $video['description'] ?? '', 'tags' => $video['tags'] ?? '', 'category' => $video['category'] ?? 'uncategorized', 'username' => $video['username'] ?? 'unknown', 'user_id' => intval($video['user_id'] ?? 0), 'content_type' => $video['content_type'] ?? 'video', 'is_live' => (bool)($video['is_live'] ?? false), 'duration' => $duration, 'duration_range' => $duration_range, 'view_count' => intval($video['view_count'] ?? 0), 'like_count' => intval($video['like_count'] ?? 0), 'upload_date' => strtotime($video['upload_date']), 'privacy' => $video['privacy'] ?? 'public' ]; } /** * Send documents to Meilisearch */ private function indexDocuments($documents) { $response = $this->apiRequest('POST', '/indexes/videos/documents', $documents); return $response !== null; } /** * Make API request to Meilisearch */ private function apiRequest($method, $endpoint, $data = null) { $url = $this->meili_host . $endpoint; $ch = curl_init($url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method); curl_setopt($ch, CURLOPT_HTTPHEADER, [ 'Authorization: Bearer ' . $this->meili_key, 'Content-Type: application/json' ]); if ($data !== null) { curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data)); } $response = curl_exec($ch); $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($http_code >= 200 && $http_code < 300) { return json_decode($response, true); } $this->logger->error("Meilisearch API error: HTTP $http_code - " . $response); return null; } } // CLI usage if (php_sapi_name() === 'cli') { $indexer = new MeilisearchIndexer(); $command = $argv[1] ?? 'help'; switch ($command) { case 'init': $indexer->initializeIndex(); break; case 'reindex': $indexer->initializeIndex(); $indexer->indexAllVideos(); break; case 'index': $video_id = $argv[2] ?? null; if ($video_id) { $indexer->indexVideo($video_id); } else { echo "Usage: php indexer.php index \n"; } break; case 'remove': $video_id = $argv[2] ?? null; if ($video_id) { $indexer->removeVideo($video_id); } else { echo "Usage: php indexer.php remove \n"; } break; default: echo "Meilisearch Indexer\n"; echo "Usage:\n"; echo " php indexer.php init - Initialize index settings\n"; echo " php indexer.php reindex - Full reindex of all videos\n"; echo " php indexer.php index - Index single video\n"; echo " php indexer.php remove - Remove video from index\n"; break; } }