andydefer / laravel-indexer
A powerful and flexible indexing system for Laravel with Eloquent support, n-gram and metaphone tokenization, and advanced search capabilities.
Requires
- php: ^8.1
- andydefer/jsonl-cache: ^0.3.21
- andydefer/laravel-task: ^4.10.42
- laravel/framework: ^12.0|^13.0|^14.0|^15.0
Requires (Dev)
- laravel/pint: ^1.29
- mockery/mockery: ^1.6
- orchestra/testbench: ^9.0|^10.0
- phpunit/phpunit: ^10.5|^11.0|^12.0
This package is auto-updated.
Last update: 2026-08-01 09:26:41 UTC
README
Table des matières
- Installation
- Configuration
- Préparer votre modèle
- Les Clusters contextuels
- Indexer des données
- Rechercher des documents
- Syntaxe de recherche
- GenericIndexerService
- CLI avec GenericIndexModelsDirective
- Tâches d'indexation programmée
- Autocomplétion
- Supprimer des documents
- Repositories
- Collections
- Référence des clusters
- Cas d'usage concrets
- Débogage et résolution des problèmes
- Performance et bonnes pratiques
1. Installation
1.1 Prérequis
- PHP 8.1 ou supérieur
- Laravel 10.x, 11.x, 12.x, 13.x, 14.x ou 15.x
1.2 Installation via Composer
composer require andydefer/laravel-indexer
1.3 Migrations
php artisan vendor:publish --tag=indexer-migrations php artisan migrate
1.4 Configuration
php artisan vendor:publish --tag=indexer-config
2. Configuration
Le fichier de configuration config/indexer.php :
<?php declare(strict_types=1); return [ /* |-------------------------------------------------------------------------- | Chemin de stockage |-------------------------------------------------------------------------- */ 'storage_path' => storage_path('indexer'), /* |-------------------------------------------------------------------------- | Types de tokens |-------------------------------------------------------------------------- */ 'token_types' => [ 'ngrams' => [ 'min_size' => 3, // Taille minimale des n-grams 'max_size' => 5, // Taille maximale des n-grams ], 'metaphone' => true, // Activation de la recherche phonétique ], /* |-------------------------------------------------------------------------- | Limite par défaut |-------------------------------------------------------------------------- */ 'default_limit' => 100, /* |-------------------------------------------------------------------------- | Cache |-------------------------------------------------------------------------- */ 'enable_cache' => true, 'cache_ttl' => 3600, /* |-------------------------------------------------------------------------- | Taille des lots |-------------------------------------------------------------------------- */ 'batch_size' => 50, /* |-------------------------------------------------------------------------- | Modèles à indexer automatiquement |-------------------------------------------------------------------------- */ 'model_indexables' => [ // App\Models\User::class, // App\Models\Hospital::class, ], /* |-------------------------------------------------------------------------- | Limites de texte |-------------------------------------------------------------------------- */ 'full_text_max_length' => 100, // Découpage des textes longs 'max_text_length' => 1000, // Longueur maximale d'un champ ];
2.1 Variables d'environnement
INDEXER_BATCH_SIZE=100 INDEXER_FULL_TEXT_MAX_LENGTH=200 INDEXER_MAX_TEXT_LENGTH=2000
3. Préparer votre modèle
Votre modèle doit implémenter l'interface Indexable.
<?php namespace App\Models; use AndyDefer\DomainStructures\Utils\StrictAssociative; use AndyDefer\LaravelCluster\ValueObjects\ClusterVO; use AndyDefer\LaravelIndexer\Contracts\Indexable; use Illuminate\Database\Eloquent\Model; class User extends Model implements Indexable { /** * Détermine si le modèle doit être indexé. */ public function shouldBeIndexed(): bool { return $this->is_active && !$this->trashed(); } /** * Retourne les données à indexer. * * Ces données seront tokenisées et rendues recherchables. */ public function getIndexableData(): StrictAssociative { return StrictAssociative::from([ 'name' => $this->name, 'email' => $this->email, 'bio' => $this->bio, 'skills' => $this->skills, 'city' => $this->city, 'country' => $this->country, 'profile' => [ 'twitter' => $this->twitter_handle, 'github' => $this->github_handle, ], ]); } /** * Retourne la classe morph. */ public function getMorphClass() { return self::class; } /** * Retourne le cluster contextuel du modèle. * * Le cluster permet de filtrer les recherches par contexte * (tenant, rôle, statut, géolocalisation, etc.). */ public function getIndexableCluster(): ClusterVO { return new ClusterVO([ 'type' => 'user', 'tenant' => $this->tenant_id, 'status' => $this->is_active ? 'active' : 'inactive', 'role' => $this->role, 'country' => $this->country, 'city' => $this->city, 'verified' => $this->email_verified_at !== null ? 'true' : 'false', ]); } }
3.1 Données imbriquées
Les données imbriquées sont automatiquement aplaties pour l'indexation :
// Dans getIndexableData() return StrictAssociative::from([ 'name' => 'John Doe', 'profile' => [ 'twitter' => '@johndoe', 'github' => 'johndoe', ], ]); // Données indexées : // - name: John Doe // - profile.twitter: @johndoe // - profile.github: johndoe
3.2 Recherche par champ spécifique
// Recherche dans le champ 'name' $query = 'john=name'; // Recherche dans plusieurs champs $query = 'john=name,email,bio'; // Recherche dans un champ imbriqué $query = 'johndoe=profile.github';
4. Les Clusters contextuels
4.1 Qu'est-ce qu'un cluster ?
Un cluster est un filtre contextuel qui permet de restreindre les recherches à un contexte spécifique.
// Exemple : Filtrer les utilisateurs actifs avec rôle admin $cluster = new ClusterVO([ 'status' => 'active', 'role' => 'admin', ]);
4.2 Création d'un cluster
use AndyDefer\LaravelCluster\ValueObjects\ClusterVO; // Création simple $cluster = new ClusterVO([ 'status' => 'active', 'role' => 'admin', 'tenant' => 'company_abc', ]); // Création avec données imbriquées $cluster = new ClusterVO([ 'user' => [ 'status' => 'active', 'role' => 'admin', ], 'addresses' => [ ['city' => 'Kinshasa', 'country' => 'RDC'], ['city' => 'Paris', 'country' => 'France'], ], ]);
4.3 Accès aux données
$cluster = new ClusterVO([ 'status' => 'active', 'role' => 'admin', 'profile' => [ 'name' => 'John Doe', ], ]); // Accès simple $status = $cluster->get('status'); // 'active' // Accès par notation pointée $name = $cluster->get('profile.name'); // 'John Doe' // Vérification d'existence if ($cluster->has('profile.name')) { echo $cluster->get('profile.name'); }
4.4 Utilisation dans le modèle
public function getIndexableCluster(): ClusterVO { return new ClusterVO([ 'type' => 'user', 'status' => $this->is_active ? 'active' : 'inactive', 'role' => $this->role, 'tenant' => $this->tenant_id, 'country' => $this->country, 'city' => $this->city, 'verified' => $this->email_verified_at !== null ? 'true' : 'false', 'has_orders' => $this->orders()->count() > 0 ? 'true' : 'false', ]); }
5. Indexer des données
5.1 Indexer un document
<?php namespace App\Services; use AndyDefer\LaravelIndexer\Contracts\IndexerInterface; use AndyDefer\LaravelIndexer\Services\Composants\IndexableRecordFactory; class UserIndexer { public function __construct( private readonly IndexerInterface $indexer ) {} public function indexUser(User $user): void { $cluster = $user->getIndexableCluster(); $record = IndexableRecordFactory::convert($user, $cluster); $this->indexer->index($record); } }
5.2 Indexer en masse
<?php namespace App\Services; use AndyDefer\LaravelIndexer\Collections\IndexableRecordCollection; use AndyDefer\LaravelIndexer\Contracts\IndexerInterface; use AndyDefer\LaravelIndexer\Services\Composants\IndexableRecordFactory; class BulkIndexer { public function __construct( private readonly IndexerInterface $indexer ) {} public function indexAllUsers(): void { $records = new IndexableRecordCollection(); User::where('is_active', true)->chunk(100, function ($users) use ($records) { foreach ($users as $user) { $cluster = $user->getIndexableCluster(); $records->add(IndexableRecordFactory::convert($user, $cluster)); } $this->indexer->indexMany($records); }); } }
5.3 Rafraîchir un document
public function updateUser(User $user): void { $user->save(); $cluster = $user->getIndexableCluster(); $record = IndexableRecordFactory::convert($user, $cluster); $this->indexer->refresh($record); }
5.4 Indexation avec GenericIndexerService
<?php namespace App\Services; use AndyDefer\LaravelIndexer\Contracts\GenericIndexerInterface; class UserIndexer { public function __construct( private readonly GenericIndexerInterface $indexer ) {} public function indexUser(int $userId): void { $this->indexer->indexById(User::class, $userId); } public function reindexAllUsers(): void { $this->indexer ->setBatchSize(50) ->setLimit(10000) ->reindexAll(User::class); } public function getIndexedCount(): int { return $this->indexer->countIndexed(User::class); } }
6. Rechercher des documents
6.1 Recherche simple
<?php namespace App\Services; use AndyDefer\LaravelIndexer\Contracts\IndexerInterface; use AndyDefer\LaravelIndexer\Records\SearchQueryRecord; use AndyDefer\LaravelIndexer\ValueObjects\SearchQueryVO; class UserSearchService { public function __construct( private readonly IndexerInterface $indexer ) {} public function search(string $query, int $limit = 20): array { $searchQuery = new SearchQueryRecord( query: new SearchQueryVO($query . '=name,email,bio'), limit: $limit ); $results = $this->indexer->search($searchQuery); $userIds = $results->getIds()->toArray(); return User::whereIn('id', $userIds)->get()->toArray(); } } // Utilisation $service = new UserSearchService($indexer); $users = $service->search('john');
6.2 Recherche avec filtres de cluster
use AndyDefer\LaravelIndexer\Records\SearchQueryRecord; use AndyDefer\LaravelIndexer\ValueObjects\SearchQueryVO; use AndyDefer\Repository\ValueObjects\ClusterQueries; $searchQuery = new SearchQueryRecord( query: new SearchQueryVO('john=name,email'), cluster_queries: new ClusterQueries([ 'cluster' => 'status=active & role=admin' ]), limit: 20 ); $results = $this->indexer->search($searchQuery);
6.3 Recherche multi-termes
// Recherche 'john' dans 'name' ET 'doe' dans 'last_name' $searchQuery = new SearchQueryRecord( query: new SearchQueryVO('john=name|doe=last_name'), limit: 20 ); $results = $this->indexer->search($searchQuery);
6.4 Vérification d'existence
use AndyDefer\LaravelIndexer\ValueObjects\IndexableFingerPrintVO; $fingerprint = new IndexableFingerPrintVO('App\Models\User|123'); $exists = $this->indexer->exists($fingerprint);
7. Syntaxe de recherche
7.1 Format général
ngram=field1,field2|ngram2=field3|ngram3=field1,field4
7.2 Exemples
| Requête | Description |
|---|---|
john=name |
Recherche "john" dans le champ "name" |
john=name,email |
Recherche "john" dans "name" ou "email" |
john=name|doe=last_name |
Recherche "john" ET "doe" |
john=profile.twitter |
Recherche dans un champ imbriqué |
7.3 Comment fonctionne la recherche ?
- Le terme est normalisé (minuscules, accents supprimés)
- Le système génère tous les n-grams possibles du terme
- Il recherche les tokens LEXICAL correspondants
- Si aucun résultat, il recherche les tokens METAPHONE (phonétique)
- Retourne les documents trouvés
Exemple :
- Indexé : "john" → tokens : ["joh", "ohn", "john"]
- Recherche "joh" → trouve "john" car "joh" est un token
- Recherche "jon" → trouve "john" via métaphone (JN → jn)
8. GenericIndexerService
Le GenericIndexerService est le service principal pour indexer vos modèles Eloquent.
8.1 Injection
<?php namespace App\Services; use AndyDefer\LaravelIndexer\Contracts\GenericIndexerInterface; class DoctorIndexer { public function __construct( private readonly GenericIndexerInterface $indexer ) {} }
8.2 Indexation
// Indexer un modèle $doctor = Doctor::find(1); $this->indexer->index($doctor); // Indexer par ID $this->indexer->indexById(Doctor::class, 1); // Indexer tous les modèles $this->indexer->indexAll(Doctor::class); // Indexer avec batch et limite $this->indexer ->setBatchSize(50) ->setLimit(1000) ->indexAll(Doctor::class);
8.3 Réindexation
// Réindexer tous les modèles (supprime puis recrée) $this->indexer->reindexAll(Doctor::class);
8.4 Suppression
// Supprimer un modèle $doctor = Doctor::find(1); $this->indexer->delete($doctor); // Supprimer par ID $this->indexer->deleteById(Doctor::class, 1); // Supprimer tous les modèles d'un type $this->indexer->deleteAll(Doctor::class);
8.5 Rafraîchissement
// Rafraîchir un modèle (supprime puis recrée si éligible) $doctor = Doctor::find(1); $this->indexer->refresh($doctor); // Rafraîchir par ID $this->indexer->refreshById(Doctor::class, 1);
8.6 Comptage et vérification
// Compter les documents indexés $count = $this->indexer->countIndexed(Doctor::class); // Vérifier si un modèle est indexé $doctor = Doctor::find(1); $exists = $this->indexer->exists($doctor); // Vérifier par ID $exists = $this->indexer->existsById(Doctor::class, 1);
8.7 Exemple complet
<?php namespace App\Services; use AndyDefer\LaravelIndexer\Contracts\GenericIndexerInterface; class IndexManagementService { public function __construct( private readonly GenericIndexerInterface $indexer ) {} public function fullReindex(): void { // Configurer les lots $this->indexer->setBatchSize(50); // Réindexer tous les types $this->indexer->reindexAll(User::class); $this->indexer->reindexAll(Product::class); $this->indexer->reindexAll(Order::class); // Vérifier le résultat $count = $this->indexer->countIndexed(User::class); echo "Utilisateurs indexés: {$count}\n"; } public function indexSpecificModels(array $ids): void { foreach ($ids as $id) { try { $this->indexer->indexById(User::class, $id); } catch (ModelNotFoundException $e) { echo "Utilisateur {$id} non trouvé\n"; } } } public function cleanupInactive(): void { // Supprimer les utilisateurs inactifs de l'index $inactiveUsers = User::where('is_active', false)->get(); foreach ($inactiveUsers as $user) { $this->indexer->delete($user); } } }
9. CLI avec GenericIndexModelsDirective
9.1 Signature
index:models {batch=50} {limit=?} {models*} {--reindex} {--count} {--delete}
9.2 Options
| Option | Description |
|---|---|
batch |
Taille des lots pour le chunking (défaut: 50) |
limit |
Nombre maximum d'éléments à indexer (optionnel) |
models* |
Liste des modèles à indexer (notation pointée: App.Models.User) |
--reindex |
Supprime puis réindexe tous les modèles |
--count |
Compte les documents indexés |
--delete |
Supprime tous les documents de l'index |
9.3 Exemples
# Indexer tous les modèles configurés ./bin/directive index:models [App.Models.User,App.Models.Hospital] # Indexer avec batch=10 et limit=5 ./bin/directive index:models 10 5 [App.Models.User] # Compter les documents indexés ./bin/directive index:models [App.Models.User] --count # Supprimer tout l'index des modèles ./bin/directive index:models [App.Models.User] --delete # Réindexer avec batch et limit ./bin/directive index:models 20 10 [App.Models.User] --reindex # Utiliser un alias ./bin/directive idx:models [App.Models.User]
9.4 Configuration
// config/indexer.php 'model_indexables' => [ App\Models\User::class, App\Models\Hospital::class, App\Models\Specialty::class, ],
10. Tâches d'indexation programmée
10.1 GenericOrchestratorRecurringTask
Cette tâche récurrente orchestre l'indexation de tous les modèles configurés.
Fonctionnement :
- Récupère les modèles configurés depuis
model_indexables - Pour chaque modèle, récupère les IDs éligibles (
shouldBeIndexed()) - Découpe les IDs par lots (
batch_size) - Enregistre une tâche
GenericIndexBatchUniqueTaskpour chaque lot
Configuration :
// config/indexer.php 'batch_size' => 50, 'model_indexables' => [ App\Models\User::class, App\Models\Hospital::class, ],
10.2 GenericIndexBatchUniqueTask
Cette tâche unique indexe un lot d'éléments.
Payload :
{
"items": [
{"modelClass": "App\\Models\\User", "id": 1},
{"modelClass": "App\\Models\\User", "id": 2},
{"modelClass": "App\\Models\\Hospital", "id": 3}
]
}
Fonctionnement :
- Reçoit une
IndexableVOCollection - Récupère toutes les instances en UNE SEULE requête par classe
- Pour chaque modèle, vérifie l'éligibilité (
shouldBeIndexed()) - Supprime le document s'il existe déjà
- Indexe le modèle avec son cluster dynamique
11. Autocomplétion
11.1 Autocomplétion simple
<?php namespace App\Services; use AndyDefer\LaravelIndexer\Repositories\IndexedTokenRepository; class AutocompleteService { public function __construct( private readonly IndexedTokenRepository $tokenRepository ) {} public function suggest(string $prefix): array { $tokens = $this->tokenRepository->autocomplete($prefix, 10); return $tokens->pluck('token')->toArray(); } } // Utilisation $suggestions = $service->suggest('jo'); // ['john', 'jonathan', 'joe', ...]
11.2 Autocomplétion avec contexte
// Autocomplétion avec cluster $tokens = $this->tokenRepository ->findByTokenAndClusterQuery($prefix, 'status=active');
11.3 Autocomplétion par champ
$tokens = $this->tokenRepository->getModel() ->newQuery() ->where('token', 'LIKE', $prefix . '%') ->where('field', 'name') ->select('token') ->distinct() ->limit(10) ->get();
12. Supprimer des documents
12.1 Suppression unitaire
use AndyDefer\LaravelIndexer\ValueObjects\IndexableFingerPrintVO; $fingerprint = new IndexableFingerPrintVO('App\Models\User|123'); $this->indexer->delete($fingerprint);
12.2 Suppression par lots
use AndyDefer\LaravelIndexer\Collections\IndexableFingerPrintVOCollection; $collection = new IndexableFingerPrintVOCollection(); $collection->add(new IndexableFingerPrintVO('App\Models\User|1')); $collection->add(new IndexableFingerPrintVO('App\Models\User|2')); $collection->add(new IndexableFingerPrintVO('App\Models\Product|5')); $this->indexer->deleteMany($collection);
12.3 Suppression par namespace
use AndyDefer\LaravelIndexer\Repositories\IndexedDocumentRepository; $repository = app(IndexedDocumentRepository::class); $repository->deleteByNamespace('App\Models\User');
12.4 Suppression par cluster
$repository->deleteByClusterQuery('status=inactive');
12.5 Vider l'index
$this->indexer->clear();
13. Repositories
13.1 IndexedDocumentRepository
use AndyDefer\LaravelIndexer\Repositories\IndexedDocumentRepository; $repository = app(IndexedDocumentRepository::class); // Recherche par fingerprint $doc = $repository->findByFingerPrint($fingerprint); $doc = $repository->findByFingerprintString('App\Models\User|123'); // Recherche par namespace $docs = $repository->findByNamespace('App\Models\User'); // Recherche par cluster $docs = $repository->findByClusterQuery('status=active & role=admin'); // Comptage $count = $repository->countByNamespace('App\Models\User'); $count = $repository->countByClusterQuery('status=active'); // Vérification d'existence $exists = $repository->existsByFingerPrint($fingerprint); $exists = $repository->existsByNamespace('App\Models\User'); // Suppression $repository->deleteByFingerPrint($fingerprint); $repository->deleteByNamespace('App\Models\User'); $repository->deleteByClusterQuery('status=inactive'); // Valeurs distinctes $namespaces = $repository->getDistinctNamespaces(); $keys = $repository->getDistinctClusterKeys(); $values = $repository->getDistinctClusterValues('status'); // Utilitaires $docs = $repository->findAllWithTokens();
13.2 IndexedTokenRepository
use AndyDefer\LaravelIndexer\Repositories\IndexedTokenRepository; use AndyDefer\LaravelIndexer\Enums\GramType; $repository = app(IndexedTokenRepository::class); // Recherche par token $tokens = $repository->findByToken('john'); $tokens = $repository->findByTokenAndField('john', 'name'); $tokens = $repository->findByTokenAndType('john', GramType::LEXICAL); // Recherche par token et cluster $tokens = $repository->findByTokenAndClusterQuery('john', 'status=active'); // Recherche par champ $tokens = $repository->findByField('name'); // Recherche par document $tokens = $repository->findByDocumentId('uuid'); $tokens = $repository->findByNamespace('App\Models\User'); // Autocomplétion $tokens = $repository->autocomplete('jo', 10); // Document IDs $ids = $repository->getDocumentIdsForToken('john'); // Comptage $count = $repository->countDistinctTokens(); $count = $repository->countByField('name'); // Suppression $repository->deleteByDocumentId('uuid'); $repository->deleteByToken('john'); // Utilitaires $tokens = $repository->getDistinctTokens(); $fields = $repository->getDistinctFields(); $repository->incrementFrequency($tokenId);
14. Collections
14.1 IndexableSearchResultCollection
$results = $this->indexer->search($query); // Accès aux résultats foreach ($results as $result) { $item = $result->item; // IndexedDocumentRecord $fingerprint = $item->fingerprint; // IndexableFingerPrintVO $field = $result->field; // string $gram = $result->gram_value; // string $type = $result->gram_type; // GramType } // Filtrage $byField = $results->filterByField('name'); $byType = $results->filterByGramType(GramType::LEXICAL); $byNamespace = $results->filterByNamespace('App\Models\User'); // Extraction $ids = $results->getIds(); // StringTypedCollection $items = $results->getItems(); // IndexableRecordCollection $fingerprints = $results->getFingerprints(); // Groupement $byField = $results->groupByField(); $byNamespace = $results->groupByNamespace();
14.2 IndexableRecordCollection
use AndyDefer\LaravelIndexer\Collections\IndexableRecordCollection; $records = new IndexableRecordCollection(); // Ajout $records->add($record); // Découpage $chunks = $records->chunk(100); // Filtrage $users = $records->filterByNamespace('App\Models\User'); $active = $records->filterByCluster('status', 'active'); // Extraction $fingerprints = $records->getFingerprints(); $clusters = $records->getClusters(); $ids = $records->getIds(); // Recherche $found = $records->findById('123'); $found = $records->findByIdAndNamespace('123', 'App\Models\User'); // Recherche textuelle $withJohn = $records->searchTextInData('John'); // Tri $sorted = $records->sortByDataField('name', true); // Pluck $names = $records->pluckDataField('name');
14.3 IndexableVOCollection
use AndyDefer\LaravelIndexer\Collections\IndexableVOCollection; use AndyDefer\LaravelIndexer\ValueObjects\IndexableVO; $collection = new IndexableVOCollection(); $collection->add(new IndexableVO(User::class, 1)); $collection->add(new IndexableVO(User::class, 2)); $collection->add(new IndexableVO(Hospital::class, 3)); // Extraction $ids = $collection->getIds(); // [1, 2, 3] $classes = $collection->getModelClasses(); // Récupération optimisée (UNE SEULE requête par classe) $instances = $collection->getModelInstances(); // Filtrage $users = $collection->filterByModelClass(User::class); // Vérification $hasId = $collection->containsId(1); $hasClass = $collection->containsModelClass(User::class); // Groupement $groups = $collection->groupByModelClass();
14.4 IndexableFingerPrintVOCollection
use AndyDefer\LaravelIndexer\Collections\IndexableFingerPrintVOCollection; use AndyDefer\LaravelIndexer\ValueObjects\IndexableFingerPrintVO; $fingerprints = new IndexableFingerPrintVOCollection(); $fingerprints->add(new IndexableFingerPrintVO('App\Models\User|1')); $fingerprints->add(new IndexableFingerPrintVO('App\Models\User|2')); // Filtrage $users = $fingerprints->filterByNamespace('App\Models\User'); // Extraction $ids = $fingerprints->getIds(); $namespaces = $fingerprints->getNamespaces(); // Vérification $hasId = $fingerprints->containsId('1'); // Recherche $fp = $fingerprints->findByValue('App\Models\User|1'); $fp = $fingerprints->findByIdAndNamespace('1', 'App\Models\User'); // Groupement $grouped = $fingerprints->groupByNamespace();
15. Référence des clusters
15.1 Syntaxe des requêtes cluster
Les requêtes cluster permettent de filtrer les documents par leurs clusters.
| Opérateur | Description | Exemple |
|---|---|---|
= |
Égalité | status=active |
!= |
Différent | status!=inactive |
< |
Inférieur | age<18 |
> |
Supérieur | age>18 |
<= |
Inférieur ou égal | age<=18 |
>= |
Supérieur ou égal | age>=18 |
& ou AND |
ET logique | status=active & role=admin |
| ou OR |
OU logique | `status=active |
* |
EXISTS | *email (la clé existe) |
# |
NOT_EXISTS | #deleted_at (la clé est absente) |
15.2 Exemples
// Égalité simple $docs = $repository->findByClusterQuery('status=active'); // AND $docs = $repository->findByClusterQuery('status=active & role=admin'); // OR $docs = $repository->findByClusterQuery('status=active | status=pending'); // Fonction SQL $docs = $repository->findByClusterQuery('COUNT(addresses) > 2'); // Sous-condition $docs = $repository->findByClusterQuery('addresses[city=Kinshasa]'); // EXISTS $docs = $repository->findByClusterQuery('*email');
15.3 Fonctions SQL disponibles
| Fonction | Description | Exemple |
|---|---|---|
COUNT(path) |
Nombre d'éléments | COUNT(addresses) > 2 |
SUM(path) |
Somme des valeurs | SUM(prices) > 500 |
AVG(path) |
Moyenne | AVG(scores) >= 85 |
LENGTH(path) |
Longueur d'une chaîne | LENGTH(name) > 5 |
JSON_LENGTH(path) |
Longueur d'un tableau JSON | JSON_LENGTH(addresses) > 2 |
16. Cas d'usage concrets
16.1 Recherche d'utilisateurs
<?php namespace App\Services; use AndyDefer\LaravelIndexer\Contracts\IndexerInterface; use AndyDefer\LaravelIndexer\Records\SearchQueryRecord; use AndyDefer\LaravelIndexer\ValueObjects\SearchQueryVO; use AndyDefer\Repository\ValueObjects\ClusterQueries; class UserSearchService { public function __construct( private readonly IndexerInterface $indexer ) {} public function searchActiveAdmins(string $query): array { $searchQuery = new SearchQueryRecord( query: new SearchQueryVO($query . '=name,email,bio'), cluster_queries: new ClusterQueries([ 'cluster' => 'status=active & role=admin' ]), limit: 20 ); $results = $this->indexer->search($searchQuery); $userIds = $results->getIds()->toArray(); return User::whereIn('id', $userIds)->get()->toArray(); } public function searchByLocation(string $query, string $city): array { $searchQuery = new SearchQueryRecord( query: new SearchQueryVO($query . '=name,email,bio'), cluster_queries: new ClusterQueries([ 'cluster' => "city=$city" ]), limit: 20 ); $results = $this->indexer->search($searchQuery); $userIds = $results->getIds()->toArray(); return User::whereIn('id', $userIds)->get()->toArray(); } }
16.2 Recherche de produits e-commerce
<?php namespace App\Services; use AndyDefer\LaravelIndexer\Contracts\IndexerInterface; use AndyDefer\LaravelIndexer\Records\SearchQueryRecord; use AndyDefer\LaravelIndexer\ValueObjects\SearchQueryVO; use AndyDefer\Repository\ValueObjects\ClusterQueries; class ProductSearchService { public function __construct( private readonly IndexerInterface $indexer ) {} public function searchProducts(string $query, array $filters): array { $conditions = []; if (isset($filters['category'])) { $conditions[] = "category={$filters['category']}"; } if (isset($filters['min_price'])) { $conditions[] = "price>={$filters['min_price']}"; } if (isset($filters['max_price'])) { $conditions[] = "price<={$filters['max_price']}"; } if (isset($filters['in_stock'])) { $conditions[] = "in_stock=" . ($filters['in_stock'] ? 'true' : 'false'); } $clusterQuery = implode(' & ', $conditions); $searchQuery = new SearchQueryRecord( query: new SearchQueryVO($query . '=name,description,tags'), cluster_queries: !empty($clusterQuery) ? new ClusterQueries([ 'cluster' => $clusterQuery ]) : null, limit: $filters['limit'] ?? 20 ); $results = $this->indexer->search($searchQuery); $productIds = $results->getIds()->toArray(); return Product::whereIn('id', $productIds)->get()->toArray(); } } // Utilisation $service = new ProductSearchService($indexer); $products = $service->searchProducts('laptop', [ 'category' => 'electronics', 'min_price' => 500, 'max_price' => 2000, 'in_stock' => true, 'limit' => 10, ]);
16.3 API REST avec recherche
<?php namespace App\Http\Controllers\Api; use AndyDefer\LaravelIndexer\Contracts\IndexerInterface; use AndyDefer\LaravelIndexer\Records\SearchQueryRecord; use AndyDefer\LaravelIndexer\ValueObjects\SearchQueryVO; use AndyDefer\Repository\ValueObjects\ClusterQueries; class UserController extends Controller { public function __construct( private readonly IndexerInterface $indexer ) {} public function search(Request $request): JsonResponse { $query = $request->get('q'); $role = $request->get('role'); $status = $request->get('status'); $city = $request->get('city'); $limit = $request->get('limit', 20); $clusterConditions = []; if ($role) { $clusterConditions[] = "role=$role"; } if ($status) { $clusterConditions[] = "status=$status"; } if ($city) { $clusterConditions[] = "city=$city"; } $searchQuery = new SearchQueryRecord( query: new SearchQueryVO($query . '=name,email,bio'), cluster_queries: !empty($clusterConditions) ? new ClusterQueries([ 'cluster' => implode(' & ', $clusterConditions) ]) : null, limit: $limit ); $results = $this->indexer->search($searchQuery); $userIds = $results->getIds()->toArray(); $users = User::whereIn('id', $userIds) ->select('id', 'name', 'email', 'role', 'city') ->get(); return response()->json([ 'data' => $users, 'meta' => [ 'total' => $users->count(), 'limit' => $limit, ] ]); } } // Exemples d'appels API // GET /api/users/search?q=john&role=admin // GET /api/users/search?q=jane&status=active&city=Paris // GET /api/users/search?q=doe&role=doctor&limit=10
17. Débogage et résolution des problèmes
17.1 Vérifier les modèles indexés
# Compter les documents indexés ./bin/directive index:models [App.Models.User] --count # Voir le SQL généré DB::enableQueryLog(); $users = User::whereCluster('clusters', 'status=active')->get(); dd(DB::getQueryLog());
17.2 Vérifier les clusters
$user = User::find(1); dd($user->getIndexableCluster()->toArray());
17.3 Problèmes courants
| Problème | Cause | Solution |
|---|---|---|
| Aucun résultat | Requête invalide | Vérifier la syntaxe de recherche |
| Résultats incomplets | Token size trop petit | Augmenter min_size dans la config |
| Indexation lente | Batch size trop petit | Augmenter batch_size |
| Erreur de syntaxe | Parenthèses mal équilibrées | Vérifier les parenthèses dans la requête |
17.4 Vérification de l'index
# Vérifier les documents indexés ./bin/directive index:models [App.Models.User] --count # Supprimer et réindexer ./bin/directive index:models [App.Models.User] --reindex
18. Performance et bonnes pratiques
18.1 Indexation
// ✅ Recommandé - Utiliser des batches $this->indexer->setBatchSize(50)->indexAll(User::class); // ✅ Recommandé - Filtrer avant d'indexer User::where('is_active', true)->chunk(100, function ($users) { // Indexer uniquement les utilisateurs actifs }); // ❌ À éviter - Indexer sans batch User::all()->each(fn($user) => $this->indexer->index($user));
18.2 Recherche
// ✅ Recommandé - Limiter les résultats $searchQuery = new SearchQueryRecord(..., limit: 20); // ✅ Recommandé - Utiliser les clusters pour filtrer $searchQuery = new SearchQueryRecord( query: $query, cluster_queries: new ClusterQueries(['cluster' => 'status=active']) ); // ❌ À éviter - Recherche sans limite $searchQuery = new SearchQueryRecord(..., limit: null);
18.3 Configuration
// Recommandations de configuration return [ 'token_types' => [ 'ngrams' => [ 'min_size' => 3, // Bon équilibre 'max_size' => 5, // Bon équilibre ], 'metaphone' => true, // Recherche phonétique ], 'batch_size' => 100, // Pour les gros volumes 'full_text_max_length' => 200, // Pour les textes longs ];
18.4 Optimisation des modèles
public function shouldBeIndexed(): bool { // ✅ Indexer uniquement les modèles actifs return $this->is_active && !$this->trashed(); } public function getIndexableCluster(): ClusterVO { // ✅ Utiliser des clusters pour le filtrage return new ClusterVO([ 'status' => $this->is_active ? 'active' : 'inactive', 'tenant' => $this->tenant_id, // ✅ Éviter les valeurs trop dynamiques ]); }
License
MIT © Andy Defer