diff --git a/app/Http/Controllers/api/GlobalSearchApiController.php b/app/Http/Controllers/api/GlobalSearchApiController.php index 82f8249..8cccc4f 100644 --- a/app/Http/Controllers/api/GlobalSearchApiController.php +++ b/app/Http/Controllers/api/GlobalSearchApiController.php @@ -7,7 +7,6 @@ use App\Models\Art; use App\Models\Book; use App\Models\Branch; use App\Models\BylawDocument; -use App\Models\BylawNode; use App\Models\Chapter; use App\Models\Division; use App\Models\Gate; @@ -27,7 +26,7 @@ class GlobalSearchApiController extends Controller use BaseApiResponse; /** - * Massive Search across ALL data types (With dynamic Date/Number Sorting & Deep Art Search) + * Massive Search across ALL data types */ public function search(Request $request) { @@ -46,56 +45,11 @@ class GlobalSearchApiController extends Controller $results = collect(); if (!empty($searchTerm)) { - $limitPerTable = 100; // Performance safeguard + $limitPerTable = 100; - // 1. Search Bylaw Documents - $bylaws = 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}%") - ->orWhereIn('id', function($subQuery) use ($searchTerm) { - $subQuery->select('document_id') - ->from('bylaw_nodes') - ->where('node_content', 'LIKE', "%{$searchTerm}%"); - }); - })->take($limitPerTable)->get()->map(function($item) { - $formatted = $this->formatBylaw($item); - $formatted['type'] = 'bylaw'; - return $formatted; - }); - $results = $results->concat($bylaws); // Changed to concat to prevent overwriting - - // 2. Search Judicial Precedents - $judicials = JudicialPrecedent::with('category') - ->where(function ($q) use ($searchTerm) { - $q->where('ruling_number', 'LIKE', "%{$searchTerm}%") - ->orWhere('subject', 'LIKE', "%{$searchTerm}%") - ->orWhere('full_text', 'LIKE', "%{$searchTerm}%"); - })->take($limitPerTable)->get()->map(function($item) { - $formatted = $this->formatJudicial($item); - $formatted['type'] = 'judicial_precedent'; - return $formatted; - }); - $results = $results->concat($judicials); - - // 3. Search Legal Opinions - $opinions = 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}%"); - })->take($limitPerTable)->get()->map(function($item) { - $formatted = $this->formatOpinion($item); - $formatted['type'] = 'legal_opinion'; - return $formatted; - }); - $results = $results->concat($opinions); - - // 4. Search Arts (Fully restored deep relational search without breaking relationships) + // ========================================================= + // 1. Search Arts FIRST (Highest Priority for Law Apps) + // ========================================================= $arts = Art::with(['law', 'law.category', 'chapter', 'part', 'volum', 'book', 'section', 'gate']) ->where(function ($q) use ($searchTerm) { $q->where('title', 'LIKE', "%{$searchTerm}%") @@ -114,9 +68,64 @@ class GlobalSearchApiController extends Controller $formatted['type'] = 'art'; return $formatted; }); - $results = $results->concat($arts); + $results = $results->concat($arts); // Arts are now at the very top of the list! + // ========================================================= + // 2. Search Bylaw Documents + // ========================================================= + $bylaws = 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}%") + ->orWhereIn('id', function($subQuery) use ($searchTerm) { + $subQuery->select('document_id') + ->from('bylaw_nodes') + ->where('node_content', 'LIKE', "%{$searchTerm}%"); + }); + })->take($limitPerTable)->get()->map(function($item) { + $formatted = $this->formatBylaw($item); + $formatted['type'] = 'bylaw'; + return $formatted; + }); + $results = $results->concat($bylaws); + + // ========================================================= + // 3. Search Judicial Precedents + // ========================================================= + $judicials = JudicialPrecedent::with('category') + ->where(function ($q) use ($searchTerm) { + $q->where('ruling_number', 'LIKE', "%{$searchTerm}%") + ->orWhere('subject', 'LIKE', "%{$searchTerm}%") + ->orWhere('full_text', 'LIKE', "%{$searchTerm}%"); + })->take($limitPerTable)->get()->map(function($item) { + $formatted = $this->formatJudicial($item); + $formatted['type'] = 'judicial_precedent'; + return $formatted; + }); + $results = $results->concat($judicials); + + // ========================================================= + // 4. Search Legal Opinions + // ========================================================= + $opinions = 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}%"); + })->take($limitPerTable)->get()->map(function($item) { + $formatted = $this->formatOpinion($item); + $formatted['type'] = 'legal_opinion'; + return $formatted; + }); + $results = $results->concat($opinions); + + // ========================================================= // 5. Search Laws and Structural Models + // ========================================================= $structuralModels = [ Law::class, Volum::class, Book::class, Division::class, Section::class, Chapter::class, Part::class, Gate::class, @@ -140,8 +149,8 @@ class GlobalSearchApiController extends Controller } } - // --- Apply Dynamic Memory Sorting --- - if ($request->has('sort_by')) { + // --- Apply Dynamic Memory Sorting ONLY if requested --- + if ($request->has('sort_by') && $request->input('sort_by') != null) { $sortBy = $request->input('sort_by'); $sortOrder = $request->input('sort', 'desc'); @@ -204,7 +213,29 @@ class GlobalSearchApiController extends Controller if (!empty($searchTerm)) { - // 1. Bylaw Documents + // 1. Search Arts FIRST for Fast Search + $artQuery = Art::with(['law', 'law.category', 'chapter', 'part', 'volum', 'book', 'section', 'gate']) + ->where(function ($q) use ($searchTerm) { + $q->where('title', 'LIKE', "%{$searchTerm}%") + ->orWhere('text', 'LIKE', "%{$searchTerm}%") + ->orWhere('number', 'LIKE', "%{$searchTerm}%") + ->orWhereHas('law', function ($sub) use ($searchTerm) { $sub->where('title', 'LIKE', "%{$searchTerm}%"); }) + ->orWhereHas('volum', function ($sub) use ($searchTerm) { $sub->where('title', 'LIKE', "%{$searchTerm}%"); }) + ->orWhereHas('book', function ($sub) use ($searchTerm) { $sub->where('title', 'LIKE', "%{$searchTerm}%"); }) + ->orWhereHas('section', function ($sub) use ($searchTerm) { $sub->where('title', 'LIKE', "%{$searchTerm}%"); }) + ->orWhereHas('chapter', function ($sub) use ($searchTerm) { $sub->where('title', 'LIKE', "%{$searchTerm}%"); }) + ->orWhereHas('part', function ($sub) use ($searchTerm) { $sub->where('title', 'LIKE', "%{$searchTerm}%"); }) + ->orWhereHas('gate', function ($sub) use ($searchTerm) { $sub->where('title', '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->concat($arts); + + // 2. Bylaw Documents $bylawQuery = BylawDocument::with(['category', 'approvalAuthority', 'promulgationAuthority']) ->where('is_published', true) ->where(function ($q) use ($searchTerm) { @@ -226,7 +257,7 @@ class GlobalSearchApiController extends Controller }); $results = $results->concat($bylaws); - // 2. Judicial Precedents + // 3. Judicial Precedents $judicialQuery = JudicialPrecedent::with('category') ->where(function ($q) use ($searchTerm) { $q->where('ruling_number', 'LIKE', "%{$searchTerm}%") @@ -241,7 +272,7 @@ class GlobalSearchApiController extends Controller }); $results = $results->concat($judicials); - // 3. Legal Opinions + // 4. Legal Opinions $opinionQuery = LegalOpinion::with('category') ->where(function ($q) use ($searchTerm) { $q->where('opinion_number', 'LIKE', "%{$searchTerm}%") @@ -257,28 +288,6 @@ class GlobalSearchApiController extends Controller }); $results = $results->concat($opinions); - // 4. Search Arts - $artQuery = Art::with(['law', 'law.category', 'chapter', 'part', 'volum', 'book', 'section', 'gate']) - ->where(function ($q) use ($searchTerm) { - $q->where('title', 'LIKE', "%{$searchTerm}%") - ->orWhere('text', 'LIKE', "%{$searchTerm}%") - ->orWhere('number', 'LIKE', "%{$searchTerm}%") - ->orWhereHas('law', function ($sub) use ($searchTerm) { $sub->where('title', 'LIKE', "%{$searchTerm}%"); }) - ->orWhereHas('volum', function ($sub) use ($searchTerm) { $sub->where('title', 'LIKE', "%{$searchTerm}%"); }) - ->orWhereHas('book', function ($sub) use ($searchTerm) { $sub->where('title', 'LIKE', "%{$searchTerm}%"); }) - ->orWhereHas('section', function ($sub) use ($searchTerm) { $sub->where('title', 'LIKE', "%{$searchTerm}%"); }) - ->orWhereHas('chapter', function ($sub) use ($searchTerm) { $sub->where('title', 'LIKE', "%{$searchTerm}%"); }) - ->orWhereHas('part', function ($sub) use ($searchTerm) { $sub->where('title', 'LIKE', "%{$searchTerm}%"); }) - ->orWhereHas('gate', function ($sub) use ($searchTerm) { $sub->where('title', '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->concat($arts); - // 5. Structural Models $structuralModels = [ Law::class, Volum::class, Book::class, Division::class, @@ -324,7 +333,6 @@ class GlobalSearchApiController extends Controller private function formatBylaw($item) { $isLocked = $this->isLockedForCurrentUserGeneric($item); - $description = $item->description; $cleanDesc = trim(strip_tags($description ?? '')); @@ -383,76 +391,73 @@ class GlobalSearchApiController extends Controller ]; } - private function formatArt($art, $searchTerm, $modelClass) + /** + * Exact 1:1 match with ArtController format mapping + */ + private function formatArt($q, $searchTerm, $modelClass) { - $law = $art->law ?? Law::find($art->law_id); + $law = $q->law ?? Law::find($q->law_id); $isLocked = ! auth()->user()->isSubscriber() && (bool) ($law?->is_locked ?? false); - $text = $art->text ?? ''; + $text = $q->text ?? ''; + $searchQuoted = preg_quote($searchTerm, '/'); - if (!empty($searchTerm) && !empty($text)) { - $pos = mb_stripos($text, $searchTerm); - if ($pos !== false) { - $start = max(0, $pos - 50); - $length = mb_strlen($searchTerm) + 100; - $context = mb_substr($text, $start, $length); - if ($start > 0) $context = '...' . $context; - if ($start + $length < mb_strlen($text)) $context = $context . '...'; - } else { - $context = $text; - } + // Exact regex match logic from ArtController + if (preg_match('/\b(.{0,50})\b('.$searchQuoted.')\b(.{0,50})\b/i', $text, $matches)) { + $context = trim($matches[1] ?? '').' '.($matches[2] ?? '').' '.trim($matches[3] ?? ''); } else { $context = $text; } return [ - 'id' => $art->id, - 'title' => $art->title, - 'number' => $art->number, + 'id' => $q->id, + 'title' => $q->title, + 'number' => $q->number, 'text' => $isLocked ? null : $context, 'is_locked' => $isLocked, - 'route' => array_values($this->route($modelClass, $art)), + 'route' => array_values($this->route($modelClass, $q)), 'category' => optional($law?->category)->name, - + 'law' => $law?->title, 'count_art' => $law?->arts?->count() ?? 0, - 'count_volums'=> $law?->volums?->count() ?? 0, + 'count_volum' => $law?->volums?->count() ?? 0, 'price' => $law?->price, 'image' => $law?->image, - 'law' => $art->law != null ? [ - 'id' => $art->law->id, - 'title' => $art->law->title, - 'number' => $art->law->number, + // Nested Relations exactly like ArtController + 'chapter' => $q->chapter != null ? [ + 'id' => $q->chapter->id, + 'title' => $q->chapter->title, + 'number' => $q->chapter->number, ] : null, - 'chapter' => $art->chapter != null ? [ - 'id' => $art->chapter->id, - 'title' => $art->chapter->title, - 'number' => $art->chapter->number, + 'part' => $q->part != null ? [ + 'id' => $q->part->id, + 'title' => $q->part->title, + 'number' => $q->part->number, ] : null, - 'part' => $art->part != null ? [ - 'id' => $art->part->id, - 'title' => $art->part->title, - 'number' => $art->part->number, + 'volum' => $q->volum != null ? [ + 'id' => $q->volum->id, + 'title' => $q->volum->title, + 'number' => $q->volum->number, ] : null, - 'volum' => $art->volum != null ? [ - 'id' => $art->volum->id, - 'title' => $art->volum->title, - 'number' => $art->volum->number, + 'law_object' => $q->law != null ? [ + 'id' => $q->law->id, + 'title' => $q->law->title, + 'number' => $q->law->number, ] : null, - 'book' => $art->book != null ? [ - 'id' => $art->book->id, - 'title' => $art->book->title, - 'number' => $art->book->number, + 'book' => $q->book != null ? [ + 'id' => $q->book->id, + 'title' => $q->book->title, + 'number' => $q->book->number, ] : null, - 'section' => $art->section != null ? [ - 'id' => $art->section->id, - 'title' => $art->section->title, - 'number' => $art->section->number, + 'section' => $q->section != null ? [ + 'id' => $q->section->id, + 'title' => $q->section->title, + 'number' => $q->section->number, ] : null, - 'gate' => $art->gate != null ? [ - 'id' => $art->gate->id, - 'title' => $art->gate->title, - 'number' => $art->gate->number, + 'gate' => $q->gate != null ? [ + 'id' => $q->gate->id, + 'title' => $q->gate->title, + 'number' => $q->gate->number, ] : null, ]; }