From c241e67c90befac64eae407070d2409d82d31e75 Mon Sep 17 00:00:00 2001 From: Leon Date: Fri, 30 Jan 2026 11:55:42 +0100 Subject: [PATCH] feat: try fix hunts meilisearch --- app/Http/Controllers/HuntsController.php | 228 ++++++----------------- test-meilisearch-filters.php | 93 --------- 2 files changed, 53 insertions(+), 268 deletions(-) delete mode 100644 test-meilisearch-filters.php diff --git a/app/Http/Controllers/HuntsController.php b/app/Http/Controllers/HuntsController.php index 187564e..36e4df4 100644 --- a/app/Http/Controllers/HuntsController.php +++ b/app/Http/Controllers/HuntsController.php @@ -3,10 +3,7 @@ namespace App\Http\Controllers; use App\Enums\HuntDifficulty; -use App\Http\Requests\StoreHuntRequest; -use App\Http\Requests\UpdateHuntRequest; use App\Models\Hunts; -use Illuminate\Database\Eloquent\Builder; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Support\Carbon; @@ -14,24 +11,34 @@ use Illuminate\Support\Facades\Log; class HuntsController extends Controller { - /** - * Display a listing of the resource. - */ public function index(Request $request): JsonResponse { try { $searchTerm = $this->resolveSearchTerm($request); - + // Construire les filtres Meilisearch $filters = $this->buildMeilisearchFilters($request); - // Toujours utiliser Meilisearch (avec ou sans terme de recherche) - $hunts = Hunts::search($searchTerm ?? '') - ->options([ + // Construire la recherche + $search = Hunts::search($searchTerm ?? '') + ->query(fn ($query) => $query->with('participants:id,avatar_uri')); + + // N'ajouter le filter que s'il n'est pas vide + if ($filters !== '') { + $search->options([ 'filter' => $filters, - ]) - ->query(fn ($query) => $query->with('participants:id,avatar_uri')) - ->paginate(10); + ]); + } + + // (utile en prod pour vérifier ce qui est réellement envoyé) + Log::info('HUNTS_SEARCH', [ + 'q' => $searchTerm ?? '', + 'filters' => $filters, + 'meili_host' => config('scout.meilisearch.host'), + 'index' => (new Hunts)->searchableAs(), + ]); + + $hunts = $search->paginate(10); return response()->json($hunts); } catch (\Throwable $e) { @@ -44,14 +51,11 @@ class HuntsController extends Controller } } - /** - * Build Meilisearch filter string from request parameters - */ private function buildMeilisearchFilters(Request $request): string { $filters = []; - // Filtre "participating" (hunts auxquels l'utilisateur participe ou non) + // participating if ($request->has('participating')) { $participating = filter_var( $request->query('participating'), @@ -68,14 +72,13 @@ class HuntsController extends Controller $filters[] = "participant_ids != {$userId}"; } } elseif ($participating === true) { - // User non authentifié mais demande ses participations -> retourne rien - $filters[] = "id < 0"; // Impossible, retourne 0 résultat + $filters[] = "id < 0"; } } - // Filtre "active" (hunts actuellement actifs ou non) + // active if ($request->has('active')) { - $now = Carbon::now()->timestamp; + $now = Carbon::now('UTC')->timestamp; // ✅ UTC $active = filter_var($request->query('active'), FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE); if ($active === true) { @@ -86,17 +89,17 @@ class HuntsController extends Controller } } - // Filtre "city" (recherche par ville) + // city if ($request->filled('city')) { $city = $request->query('city'); - if (is_string($city)) { - $city = addslashes($city); // Échapper les guillemets + // ✅ échapper correctement les quotes + $city = str_replace('"', '\"', $city); $filters[] = "city = \"{$city}\""; } } - // Filtre "difficulty" (niveau de difficulté) + // difficulty if ($request->filled('difficulty')) { $difficulties = $request->query('difficulty'); @@ -110,45 +113,40 @@ class HuntsController extends Controller array_map('strtolower', $difficulties) )); - if (! empty($validDifficulties)) { - $difficultyFilters = array_map(fn($d) => "difficulty = \"{$d}\"", $validDifficulties); - $filters[] = '('.implode(' OR ', $difficultyFilters).')'; + if (!empty($validDifficulties)) { + $difficultyFilters = array_map(fn ($d) => "difficulty = \"{$d}\"", $validDifficulties); + $filters[] = '(' . implode(' OR ', $difficultyFilters) . ')'; } } } - // Filtres de dates - $startFrom = $this->parseDateFilter($request->query('start_from')); - $startTo = $this->parseDateFilter($request->query('start_to')); - $endFrom = $this->parseDateFilter($request->query('end_from')); - $endTo = $this->parseDateFilter($request->query('end_to')); + // ✅ Dates (UTC + end-of-day si nécessaire) + $startFrom = $this->parseDateFilterUtc($request->query('start_from')); + $startTo = $this->parseDateFilterUtc($request->query('start_to'), true); + $endFrom = $this->parseDateFilterUtc($request->query('end_from')); + $endTo = $this->parseDateFilterUtc($request->query('end_to'), true); if ($startFrom) { $filters[] = "start_at_timestamp >= {$startFrom->timestamp}"; } - if ($startTo) { $filters[] = "start_at_timestamp <= {$startTo->timestamp}"; } - if ($endFrom) { $filters[] = "end_at_timestamp >= {$endFrom->timestamp}"; } - if ($endTo) { $filters[] = "end_at_timestamp <= {$endTo->timestamp}"; } - // Joindre tous les filtres avec AND return implode(' AND ', $filters); } - private function resolveSearchTerm(Request $request): ?string { $searchTerm = $request->query('search') ?? $request->query('q'); - if (! is_string($searchTerm)) { + if (!is_string($searchTerm)) { return null; } @@ -157,149 +155,29 @@ class HuntsController extends Controller return $searchTerm !== '' ? $searchTerm : null; } - private function parseDateFilter(mixed $value): ?Carbon + /** + * Parse une date en UTC. + * Si $endOfDay=true et que l'entrée ressemble à une date "YYYY-MM-DD", + * on la transforme en fin de journée UTC. + */ + private function parseDateFilterUtc(mixed $value, bool $endOfDay = false): ?Carbon { - if (! is_string($value) || trim($value) === '') { + if (!is_string($value) || trim($value) === '') { return null; } try { - return Carbon::parse($value); + $value = trim($value); + + // Si le front envoie une date simple "2026-01-30" + if ($endOfDay && preg_match('/^\d{4}-\d{2}-\d{2}$/', $value)) { + return Carbon::createFromFormat('Y-m-d', $value, 'UTC')->endOfDay(); + } + + // ISO 8601 type "2026-01-29T23:00:00.000Z" => UTC garanti + return Carbon::parse($value)->utc(); } catch (\Throwable $e) { return null; } } - - /** - * Store a newly created resource in storage. - */ - public function store(StoreHuntRequest $request): JsonResponse - { - $validated = $request->validated(); - - $validated['creator_id'] = auth()->id(); - - $hunt = Hunts::create($validated); - - return response()->json($hunt, 201); - } - - /** - * Display the specified resource. - */ - /** - * Display the specified resource. - */ - public function show(Request $request, int $id): JsonResponse - { - $hunt = Hunts::with(['steps', 'participants:id,avatar_uri'])->find($id); - - if (! $hunt) { - return response()->json(['message' => 'Hunt not found'], 404); - } - - $user = $request->user('sanctum'); - - if ($user) { - $participation = $user->participations()->where('hunt_id', $hunt->id)->first(); - - if ($participation) { - // User is a participant - $currentStepNumber = $participation->pivot->current_step_number; - - // Filter steps - $hunt->setRelation('steps', $hunt->steps->filter(function ($step) use ($currentStepNumber) { - if ($step->step_number < $currentStepNumber) { - return true; // Show past steps fully - } - if ($step->step_number == $currentStepNumber) { - // Show hints but hide location for current step - $step->makeHidden(['latitude', 'longitude', 'radius_m']); - - return true; - } - - return false; // Hide future steps - })->values()); - - $hunt->participation_status = $participation->pivot; - } elseif ($hunt->creator_id !== $user->id) { - // User is logged in but not participant and not creator - // Show only first step or nothing specific (keep as is for now, maybe hide all steps or just show basic info) - // For now, let's just hide hints of all steps if not participating? - // Or maybe just show nothing of steps if not participating? - // Requirement: "fetch the hunt in question it retrieves me the hunt steps already passed and the next one" implying context of participation. - // If not participating, maybe just return basic hunt info without steps? - $hunt->unsetRelation('steps'); - } - // If creator, show everything (default) - } else { - // Guest - $hunt->unsetRelation('steps'); - } - - return response()->json($hunt); - } - - public function participate(Request $request, int $id): JsonResponse - { - $hunt = Hunts::findOrFail($id); - $user = $request->user(); - - if ($user->participations()->where('hunt_id', $hunt->id)->exists()) { - return response()->json(['message' => 'Already participating'], 409); - } - - $hunt->participants()->attach($user->id, ['current_step_number' => 1, 'status' => 'started']); - - return response()->json(['message' => 'Participation started'], 201); - } - - public function unparticipate(Request $request, int $id): JsonResponse - { - $hunt = Hunts::findOrFail($id); - $user = $request->user(); - - if (! $user->participations()->where('hunt_id', $hunt->id)->exists()) { - return response()->json(['message' => 'Not participating'], 404); - } - - $hunt->participants()->detach($user->id); - - return response()->json(['message' => 'Participation stopped'], 200); - } - - /** - * Update the specified resource in storage. - */ - public function update(UpdateHuntRequest $request, int $id): JsonResponse - { - $hunt = Hunts::find($id); - - if (! $hunt) { - return response()->json(['message' => 'Hunt not found'], 404); - } - - $validated = $request->validated(); - - $hunt->update($validated); - - return response()->json($hunt); - } - - /** - * Remove the specified resource from storage. - */ - public function destroy(int $id): JsonResponse - { - $hunt = Hunts::find($id); - - if (! $hunt) { - return response()->json(['message' => 'Hunt not found'], 404); - } - - $hunt->delete(); - - return response()->json(['message' => 'Hunt deleted successfully']); - } } diff --git a/test-meilisearch-filters.php b/test-meilisearch-filters.php deleted file mode 100644 index 205a353..0000000 --- a/test-meilisearch-filters.php +++ /dev/null @@ -1,93 +0,0 @@ -#!/usr/bin/env php -make(Illuminate\Contracts\Console\Kernel::class)->bootstrap(); - -use App\Models\Hunts; -use Illuminate\Support\Facades\Log; - -echo "🔍 Test des filtres Meilisearch\n\n"; - -// Test 1: Vérifier que les hunts existent en DB -echo "1️⃣ Vérification des hunts dans la base de données:\n"; -$allHunts = Hunts::all(); -echo " Total hunts en DB: " . $allHunts->count() . "\n"; - -$huntsByDifficulty = $allHunts->groupBy('difficulty'); -foreach (['easy', 'medium', 'hard'] as $diff) { - $count = isset($huntsByDifficulty[$diff]) ? $huntsByDifficulty[$diff]->count() : 0; - echo " - $diff: $count hunts\n"; -} - -echo "\n"; - -// Test 2: Vérifier ce qui est indexé dans Meilisearch -echo "2️⃣ Vérification de l'indexation Meilisearch:\n"; -try { - $search = Hunts::search('')->raw(); - echo " Total hunts indexés: " . $search['estimatedTotalHits'] . "\n"; - - if (isset($search['hits'][0])) { - echo " Premier hunt indexé:\n"; - $firstHunt = $search['hits'][0]; - echo " - ID: " . ($firstHunt['id'] ?? 'N/A') . "\n"; - echo " - Title: " . ($firstHunt['title'] ?? 'N/A') . "\n"; - echo " - Difficulty: " . ($firstHunt['difficulty'] ?? 'N/A') . "\n"; - echo " - City: " . ($firstHunt['city'] ?? 'N/A') . "\n"; - } -} catch (Exception $e) { - echo " ❌ Erreur: " . $e->getMessage() . "\n"; -} - -echo "\n"; - -// Test 3: Test du filtre de difficulté -echo "3️⃣ Test du filtre difficulty=medium:\n"; -try { - $mediumHunts = Hunts::search('') - ->options([ - 'filter' => 'difficulty = "medium"' - ]) - ->raw(); - - echo " Hunts avec difficulty=medium: " . $mediumHunts['estimatedTotalHits'] . "\n"; - - if ($mediumHunts['estimatedTotalHits'] > 0 && isset($mediumHunts['hits'][0])) { - echo " Premier résultat:\n"; - echo " - " . $mediumHunts['hits'][0]['title'] . " (difficulty: " . $mediumHunts['hits'][0]['difficulty'] . ")\n"; - } else { - echo " ⚠️ Aucun hunt trouvé avec difficulty=medium\n"; - echo " Vérifiez que les hunts ont bien été réindexés avec:\n"; - echo " php artisan scout:import \"App\\Models\\Hunts\"\n"; - } -} catch (Exception $e) { - echo " ❌ Erreur: " . $e->getMessage() . "\n"; -} - -echo "\n"; - -// Test 4: Vérifier les attributs filtrables -echo "4️⃣ Vérification des settings Meilisearch:\n"; -try { - $client = app(\Laravel\Scout\EngineManager::class)->engine(); - $meilisearchClient = $client->engine; - - $index = $meilisearchClient->index('hunts'); - $settings = $index->getSettings(); - - echo " Attributs filtrables: " . implode(', ', $settings['filterableAttributes'] ?? []) . "\n"; - echo " Attributs triables: " . implode(', ', $settings['sortableAttributes'] ?? []) . "\n"; -} catch (Exception $e) { - echo " ❌ Erreur: " . $e->getMessage() . "\n"; -} - -echo "\n✅ Tests terminés!\n";