ldc-groupe/app/Models/SearchIndex.php
2026-01-15 12:27:12 +01:00

152 lines
4.2 KiB
PHP

<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Laravel\Scout\Searchable;
use Illuminate\Support\Str;
class SearchIndex extends Model
{
use Searchable;
/** @var string|null $table */
protected $table = null;
/** @var boolean $timestamps */
public $timestamps = false;
/** @var array $guarded */
protected $guarded = [];
// * METHODS
/**
* Unique name of the Meilisearch index.
*
* @return string
*/
public function searchableAs(): string
{
return config('scout.prefix') . app()->getLocale();
}
/**
* Unique primary key.
*
* @return string
*/
public function getScoutKey(): mixed
{
return "{$this->type}_{$this->model_id}";
}
/**
* Indexed data.
*
* @return array
*/
public function toSearchableArray(): array
{
return [
'id' => $this->getScoutKey(),
'type' => $this->type,
'model_id' => $this->model_id,
'id_key' => $this->id_key ?? null,
'slug' => $this->slug,
'title' => $this->title,
'content' => $this->content,
];
}
/**
* 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 = '/(?<!\w)' . preg_quote($word, '/') . '(?!\w)/iu';
// Check if current word match exactly with terms.
if (preg_match_all($pattern, $fullText, $matches, PREG_OFFSET_CAPTURE)) {
foreach ($matches[0] as $match) {
$matchPos = $match[1];
$matchLen = strlen($match[0]);
$ranges[] = [
'start' => 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 = '/(?<!\w)' . preg_quote($word, '/') . '(?!\w)/iu';
$snippet = preg_replace(
$pattern,
'<span class="search-highlight">$0</span>',
$snippet
);
}
$snippets[] = trim($snippet);
if (count($snippets) >= 3) break;
}
$result = implode('… ', $snippets);
$result = preg_replace('/\s*…\s*(?:…\s*)+/', ' … ', $result);
return trim($result);
}
}