diff --git a/app/Http/Controllers/api/GlobalSearchApiController.php b/app/Http/Controllers/api/GlobalSearchApiController.php index b10ca7c..9c67e40 100644 --- a/app/Http/Controllers/api/GlobalSearchApiController.php +++ b/app/Http/Controllers/api/GlobalSearchApiController.php @@ -24,6 +24,9 @@ class GlobalSearchApiController extends Controller { use BaseApiResponse; + /** + * Massive Search across ALL data types + */ public function search(Request $request) { $request->validate([ @@ -39,7 +42,7 @@ class GlobalSearchApiController extends Controller $results = collect(); if (!empty($searchTerm)) { - $limitPerTable = 100; + $limitPerTable = 100; // Performance safeguard // 1. Search Bylaw Documents $bylaws = BylawDocument::with(['category', 'approvalAuthority', 'promulgationAuthority']) @@ -83,11 +86,22 @@ class GlobalSearchApiController extends Controller }); $results = $results->merge($opinions); - // 4. Search Laws, Arts, and Structural Models + // 4. Search Arts (Explicitly separated for eager loading & performance) + $arts = Art::with(['law.category']) + ->where('title', 'LIKE', "%{$searchTerm}%") + ->orWhere('text', 'LIKE', "%{$searchTerm}%") + ->take($limitPerTable)->get()->map(function($item) use ($searchTerm) { + $formatted = $this->formatArt($item, $searchTerm, Art::class); + $formatted['type'] = 'art'; + return $formatted; + }); + $results = $results->merge($arts); + + // 5. Search Laws and Structural Models $structuralModels = [ Law::class, Volum::class, Book::class, Division::class, Section::class, Chapter::class, Part::class, Gate::class, - Branch::class, Art::class, + Branch::class ]; foreach ($structuralModels as $modelClass) { @@ -95,21 +109,11 @@ class GlobalSearchApiController extends Controller $modelInstance = new $modelClass; $table = $modelInstance->getTable(); - if ($table === 'art') { - $query->where('title', 'LIKE', "%{$searchTerm}%") - ->orWhere('text', 'LIKE', "%{$searchTerm}%"); - } else { - $query->where('title', 'LIKE', "%{$searchTerm}%"); - } + $query->where('title', 'LIKE', "%{$searchTerm}%"); - $items = $query->take($limitPerTable)->get()->map(function($item) use ($modelClass, $table, $searchTerm) { - if ($table === 'art') { - $formatted = $this->formatArt($item, $searchTerm, $modelClass); - $formatted['type'] = 'art'; - } else { - $formatted = $this->formatStructure($item, $modelClass); - $formatted['type'] = ($table === 'section') ? 'sections' : $table; - } + $items = $query->take($limitPerTable)->get()->map(function($item) use ($modelClass, $table) { + $formatted = $this->formatStructure($item, $modelClass); + $formatted['type'] = ($table === 'section') ? 'sections' : $table; return $formatted; }); @@ -117,9 +121,12 @@ class GlobalSearchApiController extends Controller } } + // --- Execute Manual Pagination on the Combined Collection --- $total = $results->count(); + // Slice the array for the exact page requested $paginatedItems = $results->forPage($page, $perPage)->values(); + // Build a proper Laravel paginator object so the response metadata remains identical $paginator = new LengthAwarePaginator( $paginatedItems, $total, @@ -139,6 +146,115 @@ class GlobalSearchApiController extends Controller return $this->success($responseData, 'Success', 'Global Search Results Fetched'); } + /** + * Top-level fast search for quick dropdown UI elements + */ + public function fast_search(Request $request) + { + $searchTerm = $request->input('search'); + $results = collect(); + $totalCount = 0; + + // Take a max of 3 from each table so the quick-search array doesn't become too heavy + $limitPerTable = 3; + + if (!empty($searchTerm)) { + + // 1. Bylaw Documents + $bylawQuery = BylawDocument::with(['category', 'approvalAuthority', 'promulgationAuthority']) + ->where('is_published', true) + ->where(function ($q) use ($searchTerm) { + $q->where('title', 'LIKE', "%{$searchTerm}%") + ->orWhere('description', 'LIKE', "%{$searchTerm}%") + ->orWhere('promulgation_number', 'LIKE', "%{$searchTerm}%") + ->orWhere('official_gazette_number', 'LIKE', "%{$searchTerm}%"); + }); + $totalCount += $bylawQuery->count(); + $bylaws = $bylawQuery->take($limitPerTable)->get()->map(function($item) { + $formatted = $this->formatBylaw($item); + $formatted['type'] = 'bylaw'; + return $formatted; + }); + $results = $results->merge($bylaws); + + // 2. Judicial Precedents + $judicialQuery = JudicialPrecedent::with('category') + ->where(function ($q) use ($searchTerm) { + $q->where('ruling_number', 'LIKE', "%{$searchTerm}%") + ->orWhere('subject', 'LIKE', "%{$searchTerm}%") + ->orWhere('full_text', 'LIKE', "%{$searchTerm}%"); + }); + $totalCount += $judicialQuery->count(); + $judicials = $judicialQuery->take($limitPerTable)->get()->map(function($item) { + $formatted = $this->formatJudicial($item); + $formatted['type'] = 'judicial_precedent'; + return $formatted; + }); + $results = $results->merge($judicials); + + // 3. Legal Opinions + $opinionQuery = LegalOpinion::with('category') + ->where(function ($q) use ($searchTerm) { + $q->where('opinion_number', 'LIKE', "%{$searchTerm}%") + ->orWhere('subject', 'LIKE', "%{$searchTerm}%") + ->orWhere('full_text', 'LIKE', "%{$searchTerm}%") + ->orWhere('issuing_authority', 'LIKE', "%{$searchTerm}%"); + }); + $totalCount += $opinionQuery->count(); + $opinions = $opinionQuery->take($limitPerTable)->get()->map(function($item) { + $formatted = $this->formatOpinion($item); + $formatted['type'] = 'legal_opinion'; + return $formatted; + }); + $results = $results->merge($opinions); + + // 4. Search Arts + $artQuery = Art::with(['law.category']) + ->where('title', 'LIKE', "%{$searchTerm}%") + ->orWhere('text', 'LIKE', "%{$searchTerm}%"); + $totalCount += $artQuery->count(); + $arts = $artQuery->take($limitPerTable)->get()->map(function($item) use ($searchTerm) { + $formatted = $this->formatArt($item, $searchTerm, Art::class); + $formatted['type'] = 'art'; + return $formatted; + }); + $results = $results->merge($arts); + + // 5. Structural Models + $structuralModels = [ + Law::class, Volum::class, Book::class, Division::class, + Section::class, Chapter::class, Part::class, Gate::class, + Branch::class + ]; + + foreach ($structuralModels as $modelClass) { + $query = $modelClass::query(); + $modelInstance = new $modelClass; + $table = $modelInstance->getTable(); + + $query->where('title', 'LIKE', "%{$searchTerm}%"); + + $totalCount += $query->count(); + + // Only append items if we haven't reached ~15 fast search items yet + if ($results->count() < 15) { + $items = $query->take($limitPerTable)->get()->map(function($item) use ($modelClass, $table) { + $formatted = $this->formatStructure($item, $modelClass); + $formatted['type'] = ($table === 'section') ? 'sections' : $table; + return $formatted; + }); + $results = $results->merge($items); + } + } + } + + return $this->success([ + // Return top 15 matches max for dropdown + 'items' => $results->take(15)->values(), + 'count' => $totalCount + ], 'Success', 'Global Fast Search Results'); + } + /* ----------------------------------------------------------------- * FORMATTING HELPERS * ----------------------------------------------------------------- */ @@ -198,7 +314,8 @@ class GlobalSearchApiController extends Controller private function formatArt($item, $searchTerm, $modelClass) { - $law = Law::find($item->law_id); + // Use eager loaded relation if available to prevent N+1 queries, otherwise fallback to find + $law = $item->law ?? Law::find($item->law_id); $isLocked = ! auth()->user()->isSubscriber() && (bool) $law?->is_locked; $text = $item->text ?? '';