152 lines
4.2 KiB
PHP
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);
|
|
}
|
|
}
|