54 lines
1.6 KiB
PHP
54 lines
1.6 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Chatbot\Search;
|
|
|
|
/**
|
|
* Cuts a window of about LENGTH characters out of a document, around its first match.
|
|
*
|
|
* The excerpt keeps search results small; get_document fetches the rest.
|
|
*/
|
|
final class Excerpt
|
|
{
|
|
public const LENGTH = 600;
|
|
|
|
/**
|
|
* @param list<string> $terms lower-cased, matched as prefixes like the full-text search
|
|
*/
|
|
public static function around(string $text, array $terms, int $length = self::LENGTH): string
|
|
{
|
|
$text = trim((string) preg_replace('/\s+/u', ' ', $text));
|
|
$textLength = mb_strlen($text);
|
|
|
|
if ($textLength <= $length) {
|
|
return $text;
|
|
}
|
|
|
|
$position = null;
|
|
$lower = mb_strtolower($text);
|
|
foreach ($terms as $term) {
|
|
$found = mb_strpos($lower, $term);
|
|
if (false !== $found && (null === $position || $found < $position)) {
|
|
$position = $found;
|
|
}
|
|
}
|
|
|
|
// Start a third of the window before the match, so it is read in context.
|
|
$start = max(0, min(($position ?? 0) - intdiv($length, 3), $textLength - $length));
|
|
$excerpt = mb_substr($text, $start, $length);
|
|
|
|
if ($start > 0) {
|
|
$space = mb_strpos($excerpt, ' ');
|
|
$excerpt = '…'.(false !== $space && $space < 40 ? mb_substr($excerpt, $space + 1) : $excerpt);
|
|
}
|
|
|
|
if ($start + $length < $textLength) {
|
|
$space = mb_strrpos($excerpt, ' ');
|
|
$excerpt = (false !== $space && mb_strlen($excerpt) - $space < 40 ? mb_substr($excerpt, 0, $space) : $excerpt).'…';
|
|
}
|
|
|
|
return $excerpt;
|
|
}
|
|
}
|