Initial commit
This commit is contained in:
466
app/Helpers/SearchOptimizationHelper.php
Normal file
466
app/Helpers/SearchOptimizationHelper.php
Normal file
@@ -0,0 +1,466 @@
|
||||
<?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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user