# MeiliSearch - Guide d'Architecture

## Qu'est-ce que MeiliSearch ?

MeiliSearch est un moteur de recherche full-text ultra-rapide, open-source, qui offre :
- **Rapidité** : Résultats en millisecondes
- **Tolérance aux fautes** : "Pris" trouve "Paris"
- **Recherche as-you-type** : Résultats instantanés pendant la frappe
- **Pertinence** : Algorithme de ranking intelligent
- **Facile à utiliser** : API REST simple

### Concepts Clés

#### Index
Un **index** est comme une base de données dédiée à un type de contenu. Exemple :
- Index `articles` : contient tous les articles
- Index `evenements` : contient tous les événements
- Index `global-lite` : contient un résumé de toutes les entités

Chaque index a ses propres **settings** :
- `searchableAttributes` : champs dans lesquels chercher
- `filterableAttributes` : champs utilisables pour filtrer
- `sortableAttributes` : champs utilisables pour trier

#### Document
Un **document** est un objet JSON stocké dans un index. Exemple :
```json
{
  "id": "article_123",
  "title": "Concert de musique bretonne",
  "summary": "Un grand concert aura lieu...",
  "type": "article",
  "entityId": 123
}
```

#### Recherche
La recherche se fait via une simple requête :
```
GET /indexes/articles/search?q=concert
```

MeiliSearch retourne les documents les plus pertinents, triés par score de pertinence.

---

## Architecture de Notre Implémentation

### Vue d'Ensemble

```
┌─────────────────────────────────────────────────────────────┐
│                      MeiliSearch Server                      │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐      │
│  │ global-lite  │  │   articles   │  │  evenements  │  ...  │
│  │              │  │              │  │              │       │
│  │ - title      │  │ - title      │  │ - nom        │       │
│  │ - summary    │  │ - body       │  │ - description│       │
│  │ - type       │  │ - author     │  │ - lieu       │       │
│  │ - entityId   │  │ - published  │  │ - date       │       │
│  └──────────────┘  └──────────────┘  └──────────────┘       │
└─────────────────────────────────────────────────────────────┘
         ▲                    ▲                    ▲
         │                    │                    │
         │ Synchronisation    │ Synchronisation    │
         │                    │                    │
┌────────┴────────────────────┴────────────────────┴─────────┐
│                    API Symfony                              │
│  ┌────────────────────────────────────────────────────┐    │
│  │         EntityIndexSubscriber                       │    │
│  │  (Écoute persist/update/remove sur entités)        │    │
│  └────────────────────────────────────────────────────┘    │
│          │                                   │               │
│          ▼                                   ▼               │
│  ┌──────────────────┐            ┌──────────────────┐      │
│  │ GlobalIndex      │            │ ArticleIndex     │      │
│  │ Synchronizer     │            │ Service          │ ...  │
│  └──────────────────┘            └──────────────────┘      │
│          │                                   │               │
│          └──────────────┬────────────────────┘               │
│                         ▼                                    │
│                 ┌───────────────────┐                        │
│                 │ MeiliSearchService│                        │
│                 │   (Client wrap)   │                        │
│                 └───────────────────┘                        │
│                         │                                    │
│                         ▼                                    │
│                 ┌───────────────────┐                        │
│                 │   MeiliFactory    │                        │
│                 │ (Client creation) │                        │
│                 └───────────────────┘                        │
└─────────────────────────────────────────────────────────────┘
```

### Stratégie Hybride : 2 Types d'Index

Notre implémentation utilise **deux types d'index complémentaires** :

#### 1. Index Global-Lite (`global-lite`)
**Usage** : Barre de recherche universelle

**Contenu** : Document minimal pour tous les types d'entités
```json
{
  "id": "article_123",          // Composite: type + entityId
  "title": "Mon Article",       // Titre/nom de l'entité
  "summary": "Court résumé",    // Résumé/description courte
  "type": "article",            // Type d'entité
  "entityId": 123               // ID réel de l'entité
}
```

**Avantages** :
- Recherche rapide multi-types
- Petit index = performances optimales
- Bon pour l'autocomplétion
- Résultats hétérogènes (articles + événements + ...)

**Endpoint** : `GET /api/search?q=concert`

#### 2. Index Dédiés par Entité
**Usage** : Recherche avancée avec filtres et facettes

**Exemples** :
- `articles` : Tous les champs d'Article (title, body, author, published, etc.)
- `evenements` : Tous les champs d'Evenement (nom, description, lieu, date, etc.)
- `annuaire-entrees` : Champs spécifiques à l'annuaire

**Avantages** :
- Filtres spécifiques (ex: événements par date, articles par auteur)
- Facettes (ex: grouper par catégorie)
- Tri personnalisé
- UX avancée

**Endpoints** : `GET /api/search/articles?q=concert&author=Jean`

---

## Composants Principaux

### 1. SearchableInterface

Interface que doivent implémenter toutes les entités indexables.

```php
interface SearchableInterface
{
    public function getSearchTitle(): string;
    public function getSearchSummary(): string;
    public function getSearchType(): string;
    public function getSearchEntityId(): int;
}
```

**Exemple Article** :
```php
class Article implements SearchableInterface
{
    public function getSearchTitle(): string
    {
        return $this->title;
    }

    public function getSearchSummary(): string
    {
        return substr($this->body, 0, 200);
    }

    public function getSearchType(): string
    {
        return 'article';
    }

    public function getSearchEntityId(): int
    {
        return $this->id;
    }
}
```

### 2. MeiliSearchService

Service bas-niveau qui wrappe le client MeiliSearch.

**Méthodes** :
- `index(string $indexName, array $document)` : Indexe un document
- `search(string $indexName, string $query, array $options)` : Cherche
- `deleteDocument(string $indexName, string $documentId)` : Supprime
- `bulkIndex(string $indexName, array $documents)` : Indexe en batch

### 3. IndexManagerService

Gère la création et configuration des index.

**Méthodes** :
- `createIndex(string $name)` : Crée un index
- `configureIndex(string $name, array $config)` : Configure settings
- `deleteIndex(string $name)` : Supprime un index

**Configuration typique** :
```php
$config = [
    'searchableAttributes' => ['title', 'summary'],
    'filterableAttributes' => ['type', 'published'],
    'sortableAttributes' => ['createdAt']
];
```

### 4. GlobalSearchAdapter

Transforme une entité `SearchableInterface` en document global-lite.

```php
public function toGlobalDocument(SearchableInterface $entity): array
{
    return [
        'id' => $entity->getSearchType() . '_' . $entity->getSearchEntityId(),
        'title' => $entity->getSearchTitle(),
        'summary' => $entity->getSearchSummary(),
        'type' => $entity->getSearchType(),
        'entityId' => $entity->getSearchEntityId(),
    ];
}
```

### 5. GlobalIndexSynchronizer

Synchronise les entités vers l'index global.

**Méthodes** :
- `sync(SearchableInterface $entity)` : Ajoute/met à jour dans global-lite
- `remove(string $type, int $id)` : Supprime de global-lite

### 6. Index Services par Entité

Services spécialisés pour chaque type d'entité. Exemple : `ArticleIndexService`

**Responsabilités** :
- Transformer l'entité en document complet
- Synchroniser vers index dédié
- Supprimer de l'index dédié

**Exemple** :
```php
class ArticleIndexService
{
    public function toDocument(Article $article): array
    {
        return [
            'id' => $article->getId(),
            'title' => $article->getTitle(),
            'body' => $article->getBody(),
            'author' => $article->getAuthor()?->getUsername(),
            'published' => $article->isPublished(),
            'createdAt' => $article->getCreatedAt()->getTimestamp(),
        ];
    }

    public function syncArticle(Article $article): void
    {
        $document = $this->toDocument($article);
        $this->meiliSearchService->index('articles', $document);
    }
}
```

### 7. EntityIndexSubscriber

Event subscriber Doctrine qui synchronise automatiquement les entités.

**Événements écoutés** :
- `postPersist` : Entité créée → indexe
- `postUpdate` : Entité modifiée → met à jour index
- `preRemove` : Entité supprimée → supprime de l'index

**Logique** :
```php
public function postPersist(LifecycleEventArgs $args): void
{
    $entity = $args->getObject();

    if (!$entity instanceof SearchableInterface) {
        return;
    }

    // Sync vers global-lite
    $this->globalIndexSynchronizer->sync($entity);

    // Sync vers index dédié via service approprié
    $this->getIndexService($entity)->sync($entity);
}
```

### 8. Controllers

#### GlobalSearchController
- Route : `GET /api/search`
- Query params : `q` (required, min 3 chars), `type` (optional), `limit`, `offset`
- Cherche dans `global-lite`
- Retourne : `{ hits: [...], total: 42, query: "...", ... }`

#### Contrôleurs Dédiés (ex: ArticleSearchController)
- Route : `GET /api/search/articles`
- Query params : `q`, `author`, `published`, `limit`, `offset`
- Cherche dans index `articles`
- Retourne : documents complets avec tous les champs

---

## Flux de Données

### 1. Création d'une Entité

```
User crée un Article via API
         │
         ▼
API Platform persiste Article
         │
         ▼
Doctrine déclenche postPersist
         │
         ▼
EntityIndexSubscriber capte l'événement
         │
         ├──────────────────────┬──────────────────────┐
         ▼                      ▼                      ▼
GlobalIndexSynchronizer   ArticleIndexService    ...
         │                      │
         ▼                      ▼
   Index global-lite       Index articles
```

### 2. Recherche Globale

```
User tape "concert" dans barre de recherche
         │
         ▼
GET /api/search?q=concert
         │
         ▼
GlobalSearchController
         │
         ▼
MeiliSearchService.search('global-lite', 'concert')
         │
         ▼
MeiliSearch retourne hits mixtes
         │
         ▼
API retourne: [
  { type: 'article', entityId: 123, title: '...', ... },
  { type: 'evenement', entityId: 456, title: '...', ... }
]
         │
         ▼
Front construit URLs :
  - /articles/123
  - /evenements/456
```

### 3. Recherche Dédiée

```
User recherche articles avec filtres
         │
         ▼
GET /api/search/articles?q=concert&author=Jean
         │
         ▼
ArticleSearchController
         │
         ▼
MeiliSearchService.search('articles', 'concert', {
  filter: 'author = Jean'
})
         │
         ▼
MeiliSearch retourne articles complets
         │
         ▼
API retourne documents enrichis
```

---

## Configuration MeiliSearch

### Variables d'Environnement

```env
# .env
MEILI_HOST=http://localhost:7700
MEILI_MASTER_KEY=your-master-key-here
```

### Services Symfony

```yaml
# config/services.yaml
services:
    App\Infrastructure\Search\MeiliFactory: ~

    MeiliClient:
        factory: ['@App\Infrastructure\Search\MeiliFactory', 'make']
        arguments:
            - '%env(MEILI_HOST)%'
            - '%env(MEILI_MASTER_KEY)%'
```

### Index Settings

Chaque index doit être configuré avec les bons attributs :

```php
// Index global-lite
[
    'searchableAttributes' => ['title', 'summary'],
    'filterableAttributes' => ['type'],
    'sortableAttributes' => [],
]

// Index articles
[
    'searchableAttributes' => ['title', 'body', 'author'],
    'filterableAttributes' => ['published', 'author'],
    'sortableAttributes' => ['createdAt', 'updatedAt'],
]
```

---

## Commandes Console

### Réindexation
```bash
# Réindexer toutes les entités
php bin/console search:reindex --all

# Réindexer un type spécifique
php bin/console search:reindex --entity=article
```

### Reset des Index
```bash
# Reset tous les index (supprime et recrée)
php bin/console search:reset --all

# Reset un index spécifique
php bin/console search:reset --index=articles
```

---

## Bonnes Pratiques

### 1. Que Mettre dans l'Index Global ?
- **OUI** : title, nom, libellé (champ principal)
- **OUI** : summary, description courte (max 200 chars)
- **NON** : body complet, contenu long
- **NON** : relations complexes, données dénormalisées

### 2. Quand Utiliser l'Index Dédié ?
- Recherche avec filtres (date, auteur, catégorie, etc.)
- Besoin de facettes
- Tri personnalisé
- UX avancée (recherche dans section spécifique)

### 3. Synchronisation
- **Automatique** : EntityIndexSubscriber pour persist/update/remove
- **Manuelle** : Commande console pour réindexation complète
- **Graceful** : Ne pas faire échouer l'API si MeiliSearch est down

### 4. Performance
- Utiliser `bulkIndex()` pour indexer plusieurs documents
- Limiter taille du summary (200 chars max)
- Configurer uniquement les attributs nécessaires comme filterable/sortable

---

## Ajouter une Nouvelle Entité Searchable

Voir le guide détaillé dans `ADDING_NEW_SEARCHABLE_ENTITY.md`.

**Résumé des étapes** :
1. Implémenter `SearchableInterface` sur l'entité
2. Créer `{Entity}IndexService`
3. Créer controller `{Entity}SearchController`
4. Écrire les tests
5. L'EventSubscriber gère automatiquement la sync

---

## Troubleshooting

### MeiliSearch n'indexe pas
- Vérifier que MeiliSearch tourne : `curl http://localhost:7700/health`
- Vérifier les variables d'environnement
- Vérifier les logs Symfony

### Résultats de recherche vides
- Vérifier que l'index existe : `curl http://localhost:7700/indexes`
- Vérifier qu'il y a des documents : `curl http://localhost:7700/indexes/articles/documents`
- Vérifier les `searchableAttributes` de l'index

### Erreur de connexion
- MeiliSearch est-il démarré ?
- Le port 7700 est-il accessible ?
- La master key est-elle correcte ?

---

## Ressources

- [Documentation officielle MeiliSearch](https://docs.meilisearch.com)
- [PHP SDK MeiliSearch](https://github.com/meilisearch/meilisearch-php)
- `CONSUMING_SEARCH_API.md` : Guide pour consommer l'API côté Front/BO
- `ADDING_NEW_SEARCHABLE_ENTITY.md` : Guide pour ajouter une entité
