Paillasse/portail/app/Services/Recherche/IndexRecherche.php

102 lines
3.3 KiB
PHP
Raw Normal View History

<?php
namespace App\Services\Recherche;
use App\Models\Cours;
use App\Models\Lab;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;
/**
* Bloc C (CLAUDE.md) : recherche plein texte dans les cours et labs.
* Instance Elasticsearch interne (voir ansible/roles/elasticsearch), appels
* HTTP directs plutôt qu'une bibliothèque cliente : le besoin se limite à
* indexer et chercher, pas de dépendance ajoutée pour ça.
*/
class IndexRecherche
{
public static function disponible(): bool
{
return filled(config('elasticsearch.hote'));
}
public static function reconstruire(): void
{
if (! self::disponible()) {
return;
}
Http::delete(self::url());
Http::put(self::url(), [
'mappings' => [
'properties' => [
'identifiant' => ['type' => 'keyword'],
'type' => ['type' => 'keyword'],
'titre' => ['type' => 'text', 'analyzer' => 'french'],
'contenu' => ['type' => 'text', 'analyzer' => 'french'],
],
],
]);
$documents = Lab::all()
->map(fn (Lab $lab) => [
'identifiant' => $lab->identifiant,
'type' => 'lab',
'titre' => $lab->titre,
'contenu' => trim($lab->titre.' '.implode(' ', $lab->competences ?? [])),
])
->concat(Cours::all()->map(fn (Cours $cours) => [
'identifiant' => $cours->identifiant,
'type' => 'cours',
'titre' => $cours->titre,
'contenu' => trim($cours->titre.' '.self::texteBrut($cours->contenu_markdown)),
]));
foreach ($documents as $document) {
Http::put(self::url("/_doc/{$document['type']}-{$document['identifiant']}"), $document);
}
}
/**
* @return array<int, array{type: string, identifiant: string, titre: string, extrait: string}>
*/
public static function chercher(string $texte): array
{
if (! self::disponible() || trim($texte) === '') {
return [];
}
$reponse = Http::post(self::url('/_search'), [
'query' => ['multi_match' => ['query' => $texte, 'fields' => ['titre^3', 'contenu']]],
'highlight' => ['fields' => ['contenu' => new \stdClass]],
'size' => 20,
]);
if ($reponse->failed()) {
return [];
}
/** @var array<int, array<string, mixed>> $resultats */
$resultats = $reponse->json('hits.hits') ?? [];
return collect($resultats)
->map(fn (array $hit) => [
'type' => $hit['_source']['type'],
'identifiant' => $hit['_source']['identifiant'],
'titre' => $hit['_source']['titre'],
'extrait' => $hit['highlight']['contenu'][0] ?? Str::limit($hit['_source']['contenu'], 200),
])
->all();
}
private static function url(string $chemin = ''): string
{
return 'http://'.config('elasticsearch.hote').':'.config('elasticsearch.port').'/'.config('elasticsearch.index').$chemin;
}
private static function texteBrut(string $markdown): string
{
return trim(preg_replace('/[#*`_\[\]()>-]/', ' ', $markdown) ?? '');
}
}