getLocale(); $cacheKey = self::CACHE_PREFIX . "category_hierarchy_{$locale}"; return Cache::remember($cacheKey, self::DEFAULT_TTL, function () use ($locale) { $categories = Category::where('type', 'App\Models\Tag') ->with(['translations' => function ($query) use ($locale) { $query->where('locale', $locale); }]) ->get(); return self::buildHierarchyRecursive($categories); }); } /** * Get expanded category IDs with caching */ public static function getExpandedCategoryIds(array $categoryIds): array { $cacheKey = self::CACHE_PREFIX . 'expanded_' . md5(serialize($categoryIds)); return Cache::remember($cacheKey, self::DEFAULT_TTL, function () use ($categoryIds) { $allIds = []; foreach ($categoryIds as $categoryId) { if (is_numeric($categoryId) && $categoryId > 0) { $descendants = self::getCategoryDescendants((int)$categoryId); $allIds = array_merge($allIds, $descendants); } } return array_values(array_unique($allIds)); }); } /** * Get category names by IDs with caching */ public static function getCategoryNamesByIds(array $categoryIds, string $locale = null): array { $locale = $locale ?? app()->getLocale(); $cacheKey = self::CACHE_PREFIX . "names_{$locale}_" . md5(serialize($categoryIds)); return Cache::remember($cacheKey, 300, function () use ($categoryIds, $locale) { $categories = Category::whereIn('id', $categoryIds) ->with(['translations' => function ($query) use ($locale) { $query->where('locale', $locale); }]) ->get(); return $categories->map(function ($category) { return $category->translation ? $category->translation->name : null; })->filter()->values()->toArray(); }); } /** * Clear all search-related caches using app locales */ public static function clearSearchCaches(): void { // Use your supported locales $locales = ['en', 'nl', 'fr', 'es', 'de']; foreach ($locales as $locale) { Cache::forget(self::CACHE_PREFIX . "category_hierarchy_{$locale}"); } // Clear pattern-based cache keys if (Cache::getStore() instanceof \Illuminate\Cache\RedisStore) { $cacheKeys = Cache::getRedis()->keys(self::CACHE_PREFIX . '*'); if (!empty($cacheKeys)) { Cache::getRedis()->del($cacheKeys); } } Log::info('Search caches cleared'); } /** * Build search query suggestions based on category names */ public static function buildSearchSuggestions(array $categoryNames, string $locale = null): array { $locale = $locale ?? app()->getLocale(); $suggestions = []; foreach ($categoryNames as $categoryName) { // Add exact match $suggestions[] = [ 'text' => $categoryName, 'weight' => 10, 'input' => [$categoryName], ]; // Add partial matches $words = explode(' ', $categoryName); if (count($words) > 1) { foreach ($words as $word) { if (strlen($word) > 2) { $suggestions[] = [ 'text' => $word, 'weight' => 5, 'input' => [$word], ]; } } } } return array_unique($suggestions, SORT_REGULAR); } /** * Optimize search results by applying relevance scoring including location */ public static function optimizeSearchResults(array $results, array $searchTerms = [], array $userLocation = []): array { $optimized = []; foreach ($results as $result) { $score = $result['score'] ?? 1; // Boost based on model type $modelBoost = self::getModelTypeBoost($result['model'] ?? ''); $score *= $modelBoost; // Boost based on highlight quality $highlightBoost = self::getHighlightBoost($result['highlight'] ?? []); $score *= $highlightBoost; // Boost based on data completeness $completenessBoost = self::getCompletenessBoost($result); $score *= $completenessBoost; // Boost based on recency $recencyBoost = self::getRecencyBoost($result); $score *= $recencyBoost; // Boost based on location proximity $locationBoost = self::getLocationBoost($result, $userLocation); $score *= $locationBoost; $result['optimized_score'] = $score; $optimized[] = $result; } // Sort by optimized score primarily, with location as a tie-breaker usort($optimized, function ($a, $b) { // Primary sort: optimized score (higher scores first) $aScore = $a['optimized_score'] ?? 0; $bScore = $b['optimized_score'] ?? 0; $scoreDiff = $bScore <=> $aScore; if ($scoreDiff !== 0) { return $scoreDiff; } // Secondary sort: location distance (closer locations first) $aDistance = $a['location_proximity']['distance'] ?? 9; $bDistance = $b['location_proximity']['distance'] ?? 9; return $aDistance <=> $bDistance; }); return $optimized; } /** * Generate search analytics data */ public static function trackSearchAnalytics(array $categoryIds, int $totalResults, float $executionTime): void { $analyticsData = [ 'timestamp' => now(), 'category_ids' => $categoryIds, 'total_results' => $totalResults, 'execution_time' => $executionTime, 'locale' => app()->getLocale(), 'user_id' => auth()->id(), ]; // Store in cache for recent searches $recentSearchesKey = self::CACHE_PREFIX . 'recent_searches_' . (auth()->id() ?? 'guest'); $recentSearches = Cache::get($recentSearchesKey, []); array_unshift($recentSearches, $analyticsData); $recentSearches = array_slice($recentSearches, 0, 10); // Keep last 10 searches Cache::put($recentSearchesKey, $recentSearches, now()->addHours(24)); // Log for analysis Log::info('Search performed', $analyticsData); } /** * Get search performance metrics */ public static function getSearchMetrics(): array { $userId = auth()->id(); $recentSearchesKey = self::CACHE_PREFIX . 'recent_searches_' . ($userId ?? 'guest'); $recentSearches = Cache::get($recentSearchesKey, []); if (empty($recentSearches)) { return [ 'total_searches' => 0, 'avg_results' => 0, 'avg_execution_time' => 0, 'popular_categories' => [], ]; } $totalSearches = count($recentSearches); $totalResults = array_sum(array_column($recentSearches, 'total_results')); $totalExecutionTime = array_sum(array_column($recentSearches, 'execution_time')); // Get popular categories $allCategories = []; foreach ($recentSearches as $search) { $allCategories = array_merge($allCategories, $search['category_ids']); } $popularCategories = array_count_values($allCategories); arsort($popularCategories); return [ 'total_searches' => $totalSearches, 'avg_results' => $totalSearches > 0 ? round($totalResults / $totalSearches, 2) : 0, 'avg_execution_time' => $totalSearches > 0 ? round($totalExecutionTime / $totalSearches, 4) : 0, 'popular_categories' => array_slice($popularCategories, 0, 5, true), ]; } /** * Build hierarchy recursively */ private static function buildHierarchyRecursive($categories, $parentId = null): array { $hierarchy = []; foreach ($categories as $category) { if ($category->parent_id == $parentId) { $categoryData = [ 'id' => $category->id, 'name' => $category->translation->name ?? $category->name, 'color' => $category->relatedColor, 'parent_id' => $category->parent_id, 'children' => self::buildHierarchyRecursive($categories, $category->id) ]; $hierarchy[] = $categoryData; } } return $hierarchy; } /** * Get category descendants with caching */ private static function getCategoryDescendants(int $categoryId): array { $cacheKey = self::CACHE_PREFIX . "descendants_{$categoryId}"; return Cache::remember($cacheKey, self::DEFAULT_TTL, function () use ($categoryId) { $category = Category::find($categoryId); if (!$category) { return [$categoryId]; } try { $descendants = $category->descendants()->pluck('id')->toArray(); return array_merge([$categoryId], array_map('intval', $descendants)); } catch (\Exception $e) { Log::warning("Failed to get descendants for category {$categoryId}: " . $e->getMessage()); return [$categoryId]; } }); } /** * Get model type boost factor using timebank-cc configuration */ private static function getModelTypeBoost(string $modelClass): float { $type = strtolower(class_basename($modelClass)); return timebank_config('main_search_bar.boosted_models.' . $type, 1.0); } /** * Get highlight quality boost */ private static function getHighlightBoost(array $highlights): float { if (empty($highlights)) { return 1.0; } $totalHighlights = 0; foreach ($highlights as $fieldHighlights) { $totalHighlights += count($fieldHighlights); } // More highlights = higher relevance return 1.0 + (min($totalHighlights, 10) * 0.05); } /** * Get data completeness boost */ private static function getCompletenessBoost(array $result): float { $completenessFactors = 0; $totalFactors = 0; // Check various completeness factors $checks = [ 'about' => !empty($result['about'] ?? ''), 'about_short' => !empty($result['about_short'] ?? ''), 'subtitle' => !empty($result['subtitle'] ?? ''), 'photo' => !empty($result['photo'] ?? ''), 'location' => !empty($result['location'] ?? ''), 'category' => !empty($result['category'] ?? ''), ]; foreach ($checks as $factor => $isPresent) { $totalFactors++; if ($isPresent) { $completenessFactors++; } } if ($totalFactors === 0) { return 1.0; } $completenessRatio = $completenessFactors / $totalFactors; return 1.0 + ($completenessRatio * 0.2); // Up to 20% boost } /** * Get location-based boost based on proximity to user */ private static function getLocationBoost(array $result, array $userLocation): float { if (empty($userLocation) || !isset($result['location_proximity'])) { return 1.0; } $proximity = $result['location_proximity']; // Location proximity boost factors $locationBoosts = [ 'same_district' => 1.1, 'same_city' => 1.05, 'same_division' => 1.02, 'same_country' => 1.0, 'different_country' => 1.0, 'no_location' => 1.0, 'unknown' => 1.0, 'error' => 1.0, ]; return $locationBoosts[$proximity['level'] ?? 'unknown'] ?? 1.0; } /** * Get recency boost based on creation/update time */ private static function getRecencyBoost(array $result): float { // This would need to be implemented based on your specific timestamp fields // For now, return neutral boost return 1.0; } /** * Generate location-aware search analytics */ public static function trackLocationSearchAnalytics(array $categoryIds, int $totalResults, float $executionTime, array $locationHierarchy = []): void { $analyticsData = [ 'timestamp' => now(), 'category_ids' => $categoryIds, 'total_results' => $totalResults, 'execution_time' => $executionTime, 'locale' => app()->getLocale(), 'user_id' => auth()->id(), 'location_hierarchy' => [ 'country_id' => $locationHierarchy['country']->id ?? null, 'division_id' => $locationHierarchy['division']->id ?? null, 'city_id' => $locationHierarchy['city']->id ?? null, 'district_id' => $locationHierarchy['district']->id ?? null, ], ]; // Store in cache for recent searches $recentSearchesKey = self::CACHE_PREFIX . 'recent_searches_' . (auth()->id() ?? 'guest'); $recentSearches = Cache::get($recentSearchesKey, []); array_unshift($recentSearches, $analyticsData); $recentSearches = array_slice($recentSearches, 0, 10); // Keep last 10 searches Cache::put($recentSearchesKey, $recentSearches, now()->addHours(24)); // Log for analysis Log::info('Location-aware search performed', $analyticsData); } /** * Validate search parameters using timebank-cc configuration */ public static function validateSearchParams(array $params): array { $validated = []; // Validate category IDs if (isset($params['category_ids']) && is_array($params['category_ids'])) { $validated['category_ids'] = array_filter($params['category_ids'], function ($id) { return is_numeric($id) && $id > 0; }); } // Validate locale using your base language and supported locales if (isset($params['locale'])) { // Use your supported locales (en, nl, fr, es, de from your config) $availableLocales = ['en', 'nl', 'fr', 'es', 'de']; $validated['locale'] = in_array($params['locale'], $availableLocales) ? $params['locale'] : timebank_config('base_language', 'en'); } // Validate limit using your max_results if (isset($params['limit'])) { $maxResults = timebank_config('main_search_bar.search.max_results', 50); $validated['limit'] = max(1, min((int)$params['limit'], $maxResults)); } return $validated; } /** * Build cache key for search results compatible with timebank-cc naming */ public static function buildSearchCacheKey(array $params): string { ksort($params); // Ensure consistent ordering return 'category_search_results_' . md5(serialize($params)); } }