Files
timebank-cc-public/app/Helpers/SearchOptimizationHelper.php
Ronald Huynen 2547717edb Initial commit
2026-03-23 21:37:59 +01:00

467 lines
16 KiB
PHP

<?php
namespace App\Helpers;
use App\Models\Category;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
class SearchOptimizationHelper
{
private const CACHE_PREFIX = 'search_optimization_';
private const DEFAULT_TTL = 3600; // 1 hour
/**
* Build optimized category hierarchy with caching
*/
public static function getCategoryHierarchy(string $locale = null): array
{
$locale = $locale ?? app()->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));
}
}