102 lines
3.3 KiB
PHP
102 lines
3.3 KiB
PHP
|
|
<?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) ?? '');
|
||
|
|
}
|
||
|
|
}
|