feat: add MCP server for the website chatbot over a mirrored TYPO3 content feed
This commit is contained in:
1 parent
ff7ddf7a9e
commit
1b4384e3f3
52 files changed
+5117
-4
No files matched your search
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Chatbot\Search;
|
||||
|
||||
use App\Repository\ContentDocumentRepository;
|
||||
|
||||
/**
|
||||
* Full-text search over the mirrored website content.
|
||||
*
|
||||
* Ranked by relevance, weighted by type in the order people need them: what they book
|
||||
* (products, hotels), where they go (destinations, concepts), general pages, then blog
|
||||
* posts. The weights are multipliers on the relevance score, so a much better match of a
|
||||
* lower type still wins.
|
||||
*/
|
||||
class ContentSearch
|
||||
{
|
||||
/**
|
||||
* The tool's `type` values. `destination` covers region, country and city.
|
||||
*/
|
||||
public const TYPES = [
|
||||
'page' => ['page'],
|
||||
'product' => ['product'],
|
||||
'hotel' => ['hotel'],
|
||||
'destination' => ['region', 'country', 'city'],
|
||||
'concept' => ['concept'],
|
||||
'news' => ['news'],
|
||||
];
|
||||
|
||||
private const TYPE_WEIGHTS = [
|
||||
'product' => 1.4,
|
||||
'hotel' => 1.4,
|
||||
'region' => 1.2,
|
||||
'country' => 1.2,
|
||||
'city' => 1.2,
|
||||
'concept' => 1.2,
|
||||
'page' => 1.0,
|
||||
'news' => 0.8,
|
||||
];
|
||||
|
||||
public function __construct(private readonly ContentDocumentRepository $repository)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $type a key of TYPES
|
||||
*
|
||||
* @return list<array{type: string, title: string, url: string, site: ?string, breadcrumb: ?string, excerpt: string}>
|
||||
*/
|
||||
public function search(string $query, ?string $type, ?string $site, int $limit): array
|
||||
{
|
||||
$booleanQuery = ContentSearchQuery::booleanQuery($query);
|
||||
|
||||
if (null === $booleanQuery) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$rows = $this->repository->searchFulltext($booleanQuery, null === $type ? [] : self::TYPES[$type], $site, self::TYPE_WEIGHTS, $limit);
|
||||
$terms = ContentSearchQuery::terms($query);
|
||||
|
||||
return array_map(static fn (array $row): array => [
|
||||
'type' => self::publicType($row['type']),
|
||||
'title' => $row['title'],
|
||||
'url' => $row['url'],
|
||||
'site' => $row['site'],
|
||||
'breadcrumb' => $row['breadcrumb'],
|
||||
'excerpt' => Excerpt::around($row['body_md'], $terms),
|
||||
], $rows);
|
||||
}
|
||||
|
||||
/**
|
||||
* The tool's name for a stored type.
|
||||
*/
|
||||
public static function publicType(string $type): string
|
||||
{
|
||||
foreach (self::TYPES as $public => $types) {
|
||||
if (in_array($type, $types, true)) {
|
||||
return $public;
|
||||
}
|
||||
}
|
||||
|
||||
return $type;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user