meili_host = getenv('MEILI_HOST') ?: 'http://meilisearch:7700'; $this->meili_key = getenv('MEILI_MASTER_KEY') ?: 'changeme_meilisearch_master_key'; $this->logger = new VLogger('search_api'); } public function search() { // Validate query $query = $_GET['q'] ?? ''; if (empty(trim($query))) { return $this->error('Search query is required', 400); } // Build search parameters $params = [ 'q' => $query, 'limit' => min(intval($_GET['limit'] ?? 20), 100), 'offset' => (intval($_GET['page'] ?? 1) - 1) * min(intval($_GET['limit'] ?? 20), 100) ]; // Build filters $filters = []; if (!empty($_GET['category'])) { $filters[] = 'category = "' . $this->escapeFilter($_GET['category']) . '"'; } if (!empty($_GET['duration'])) { $filters[] = 'duration_range = "' . $this->escapeFilter($_GET['duration']) . '"'; } if (!empty($_GET['content_type'])) { $filters[] = 'content_type = "' . $this->escapeFilter($_GET['content_type']) . '"'; } if (!empty($_GET['date_from'])) { $filters[] = 'upload_date >= ' . intval($_GET['date_from']); } if (!empty($_GET['date_to'])) { $filters[] = 'upload_date <= ' . intval($_GET['date_to']); } // Only show public/approved content $filters[] = 'privacy = "public"'; if (!empty($filters)) { $params['filter'] = implode(' AND ', $filters); } // Sorting if (!empty($_GET['sort'])) { $sort_field = $_GET['sort']; $sort_order = $_GET['order'] ?? 'desc'; $params['sort'] = ["$sort_field:$sort_order"]; } // Facets for filter options $params['facets'] = ['category', 'content_type', 'duration_range']; // Execute search $response = $this->apiRequest('POST', '/indexes/videos/search', $params); if ($response === null) { return $this->error('Search service unavailable', 503); } // Track search query for trending $this->trackQuery($query); // Format response return $this->success([ 'query' => $query, 'results' => $this->formatResults($response['hits'] ?? []), 'total' => $response['estimatedTotalHits'] ?? 0, 'page' => intval($_GET['page'] ?? 1), 'limit' => intval($_GET['limit'] ?? 20), 'facets' => $response['facetDistribution'] ?? [], 'processing_time_ms' => $response['processingTimeMs'] ?? 0 ]); } private function formatResults($hits) { return array_map(function($hit) { return [ 'video_id' => $hit['video_id'], 'title' => $hit['title'], 'description' => $hit['description'], 'category' => $hit['category'], 'username' => $hit['username'], 'duration' => $hit['duration'], 'view_count' => $hit['view_count'], 'like_count' => $hit['like_count'], 'upload_date' => $hit['upload_date'], 'is_live' => $hit['is_live'], 'thumbnail' => $this->getThumbnailUrl($hit['video_id']), 'url' => '/watch?v=' . $hit['video_id'] ]; }, $hits); } private function getThumbnailUrl($video_id) { // Use existing thumbnail logic from the platform return "/thumbs/$video_id.jpg"; } private function trackQuery($query) { global $db; // Store in trending queries table (create if doesn't exist) $query_escaped = $db->escape($query); $sql = "INSERT INTO db_search_queries (query, search_count, last_searched) VALUES ('$query_escaped', 1, NOW()) ON DUPLICATE KEY UPDATE search_count = search_count + 1, last_searched = NOW()"; $db->execute($sql); } private function escapeFilter($value) { return addslashes($value); } 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; } private function success($data) { echo json_encode([ 'success' => true, 'data' => $data ]); exit; } private function error($message, $code = 400) { http_response_code($code); echo json_encode([ 'success' => false, 'error' => $message ]); exit; } } // Execute search $api = new SearchAPI(); $api->search();