validated()[$queryKey]; return view('front.pages.search', compact('searchTerm')); } /** * Load results from search. * * @param \App\Http\Requests\Front\Search\SearchAjaxRequest $request * @return \Illuminate\Http\JsonResponse */ public function ajaxSearchLoadResults(SearchAjaxRequest $request): \Illuminate\Http\JsonResponse { $class = $request->validated()['class']; $searchTerm = $request->validated()['search']; $currentPage = (int) $request->validated()['page']; $currentLocale = $request->validated()['locale']; $perPage = 5; app()->setLocale($currentLocale); $searchQuery = $this->buildExactSearchQuery($searchTerm); $models = $class::search($searchQuery, function ($engine, $query, $options) use ($class, $currentLocale) { $options['attributesToSearchOn'] = $class::searchTransFields($currentLocale); return $engine->search($query, $options); }) ->where('status', $class::searchStatusEnum()) ->paginateRaw(perPage: $perPage, page: $currentPage); // Render items. $itemsHtml = collect($models['hits'])->map(fn ($result) => view('components.front.search.card', [ 'class' => $class, 'result' => $result, 'mainField' => $class::searchMainField(), 'routeName' => $class::searchRouteName($result['id_key'] ?? null), 'customBreadcrumbs' => true, 'searchTerm' => $searchTerm, 'currentLocale' => $currentLocale, ])->render() )->implode(''); // Pagination. $paginationHtml = view('components.front.search.pagination', [ 'models' => $models, ])->render(); return response()->json([ 'items' => $itemsHtml, 'pagination' => $paginationHtml, 'totalHits' => $models['totalHits'], ]); } /** * Set strict search term. * * @return string */ private function buildExactSearchQuery(string $searchTerm): string { $words = preg_split('/\s+/', trim($searchTerm), -1, PREG_SPLIT_NO_EMPTY); if (count($words) === 1) { return '"' . $words[0] . '"'; } $exactTerms = array_map(function($word) { return $word . '*'; }, $words); return '+' . implode(' +', $exactTerms); } /** * Highlights all search terms in a text. * * @param string $text * @param string $searchTerm * @return string */ public static function highlightTerms(string $text, string $searchTerm, int $contextLength = 150): string { if (trim($searchTerm) === '') { return $text; } // Clean terms list. $words = preg_split('/\s+/', $searchTerm, -1, PREG_SPLIT_NO_EMPTY); if (empty($words)) { return $text; } $fullText = strip_tags($text); $textLength = strlen($fullText); $ranges = []; // Collection of relevant ranges around each word. foreach ($words as $word) { $pattern = '/(? max(0, $matchPos - $contextLength), 'end' => min($textLength, $matchPos + $matchLen + $contextLength), ]; } } } // Return the entire text if there isn't matches. if (empty($ranges)) { return $text; } // Merging overlapping intervals. usort($ranges, fn($a, $b) => $a['start'] <=> $b['start']); $merged = []; foreach ($ranges as $r) { if (empty($merged) || $r['start'] > $merged[array_key_last($merged)]['end']) { $merged[] = $r; } else { $merged[array_key_last($merged)]['end'] = max( $merged[array_key_last($merged)]['end'], $r['end'] ); } } $snippets = []; foreach ($merged as $r) { $snippet = substr($fullText, $r['start'], $r['end'] - $r['start']); // Cut cleanly without breaking the words if ($r['start'] > 0) { $snippet = preg_replace('/^\S*\s/', '…', $snippet, 1); } if ($r['end'] < $textLength) { $snippet = preg_replace('/\s\S*$/', '…', $snippet, 1); } // Highlighting searched words foreach ($words as $word) { $pattern = '/(?$0', $snippet ); } $snippets[] = trim($snippet); if (count($snippets) >= 3) break; } $result = implode('… ', $snippets); $result = preg_replace('/\s*…\s*(?:…\s*)+/', ' … ', $result); return trim($result); } }