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.20
- andydefer/laravel-task: ^4.10.28
- 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-07-27 03:41:59 UTC
README
Table des matières
- Installation
- Préparer votre modèle
- Les Clusters
- Indexer des données
- GenericIndexerService
- GenericIndexModelsDirective
- GenericOrchestratorRecurringTask
- GenericIndexBatchUniqueTask
- Rechercher
- Autocomplétion
- Supprimer
- Repositories
- Collections
Installation
composer require andydefer/laravel-indexer
Migrations
php artisan vendor:publish --tag=indexer-migrations php artisan migrate
Configuration
php artisan vendor:publish --tag=indexer-config
// config/indexer.php return [ 'token_types' => [ 'ngrams' => [ 'min_size' => 3, 'max_size' => 5, ], 'metaphone' => true, ], 'default_limit' => 100, 'batch_size' => 50, 'model_indexables' => [ App\Models\User::class, App\Models\Hospital::class, App\Models\Specialty::class, ], 'full_text_max_length' => 100, 'max_text_length' => 1000, ];
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\LaravelIndexer\Contracts\Indexable; use AndyDefer\LaravelIndexer\ValueObjects\ClusterVO; 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; } /** * Retourne les données à indexer. */ public function getIndexableData(): StrictAssociative { return StrictAssociative::from([ 'name' => $this->name, 'email' => $this->email, 'bio' => $this->bio, 'skills' => $this->skills, 'role' => $this->role, 'profile' => [ 'twitter' => $this->twitter, 'github' => $this->github, ], ]); } /** * Retourne l'identifiant unique du modèle. */ public function getKey() { return $this->id; } /** * Retourne le nom de la classe morph. */ public function getMorphClass() { return self::class; } /** * Génère le cluster dynamique du modèle. * * Le cluster permet de filtrer les recherches par contexte * (tenant, environnement, rôle, statut, etc.). */ public function getIndexableCluster(): ClusterVO { return new ClusterVO('type:user') ->withTernary('status', (bool) $this->is_active, 'active', 'inactive') ->whenNotEmpty('role', $this->role) ->whenBool('verified', $this->email_verified_at !== null); } }
Les Clusters
Le cluster est un filtre contextuel permettant de filtrer les recherches par contexte (tenant, environnement, rôle, statut, etc.). C'est un élément clé de l'indexation qui permet une recherche multi-contextes.
Format du cluster
key1:value1|key2:value2|key3:value3
| Élément | Description |
|---|---|
key:value |
Paire clé-valeur (une clé = une valeur) |
| ` | ` |
@AND / @OR / @NOT |
Mode de recherche (optionnel pour le stockage, obligatoire pour la recherche) |
Caractères autorisés :
- Clés :
a-z,A-Z,0-9,_uniquement - Valeurs : Tous les caractères (libre)
Créer un Cluster
Constructeur classique
use AndyDefer\LaravelIndexer\ValueObjects\ClusterVO; // Cluster simple (stockage) $cluster = new ClusterVO('tenant:company_abc'); // Cluster multi-attributs (stockage) $cluster = new ClusterVO('tenant:company_abc|env:production|region:europe'); // Cluster avec mode (recherche) $cluster = new ClusterVO('tenant:company_abc|env:production@AND'); $cluster = new ClusterVO('role_doctor:true|role_admin:true@OR'); $cluster = new ClusterVO('status:inactive@NOT');
Méthode make() - Builder fluent
// Stockage (sans mode) $cluster = ClusterVO::make('type', 'user') ->with('role', 'doctor') ->with('status', 'active'); // Résultat: type:user|role:doctor|status:active // Recherche (avec mode) $cluster = ClusterVO::make('type', 'user', 'AND') ->with('role', 'doctor') ->with('status', 'active'); // Résultat: type:user|role:doctor|status:active@AND
Lire un Cluster
get(string $key): ?string
Retourne la valeur pour une clé donnée.
$cluster = new ClusterVO('tenant:company_abc|env:production'); $cluster->get('tenant'); // 'company_abc' $cluster->get('env'); // 'production' $cluster->get('unknown'); // null
has(string $key): bool
Vérifie si une clé existe.
$cluster = new ClusterVO('tenant:company_abc|env:production'); $cluster->has('tenant'); // true $cluster->has('unknown'); // false
all(): array
Retourne toutes les paires clé-valeur.
$cluster = new ClusterVO('type:user|role:doctor|status:active'); $cluster->all(); // [ // 'type' => 'user', // 'role' => 'doctor', // 'status' => 'active', // ]
Ajouter des valeurs
with(string $key, string $value): self
Ajoute une paire clé-valeur.
$cluster = new ClusterVO('type:user') ->with('role', 'doctor') ->with('status', 'active'); // type:user|role:doctor|status:active
withIf(bool $condition, string $key, string $value): self
Ajoute une paire uniquement si la condition est vraie.
$cluster = new ClusterVO('type:user') ->withIf($isAdmin, 'role', 'admin') ->withIf($isActive, 'status', 'active');
withTernary(string $key, bool $condition, string $trueValue, string $falseValue): self
Ajoute une paire clé-valeur en fonction d'un booléen.
$cluster = new ClusterVO('type:user') ->withTernary('status', $isActive, 'active', 'inactive') ->withTernary('verified', $isVerified, 'true', 'false'); // status:active|verified:true (si les conditions sont vraies)
Méthodes conditionnelles
Ces méthodes ajoutent des paires uniquement si certaines conditions sont remplies.
whenNotEmpty(string $key, mixed $value): self
Ajoute une valeur si elle n'est pas vide.
$cluster = new ClusterVO('type:user') ->whenNotEmpty('city', $user->city) ->whenNotEmpty('country', $user->country) ->whenNotEmpty('role', $user->role); // type:user|city:Paris|country:France|role:doctor
whenNotNull(string $key, mixed $value): self
Ajoute une valeur si elle n'est pas nulle.
$cluster = new ClusterVO('type:user') ->whenNotNull('role', $user->role) ->whenNotNull('tenant', $user->tenant_id); // type:user|role:doctor|tenant:company_abc
whenNumeric(string $key, mixed $value): self
Ajoute une valeur si elle est numérique.
$cluster = new ClusterVO('type:user') ->whenNumeric('age', 25) // ✅ 25 ->whenNumeric('age', '30') // ✅ '30' ->whenNumeric('score', null) // ❌ null ->whenNumeric('rating', 'not a number'); // ❌ Non numérique // type:user|age:25|age:30
whenBool(string $key, mixed $value): self
Ajoute une valeur si elle est booléenne.
$cluster = new ClusterVO('type:user') ->whenBool('verified', true) // ✅ true → 'true' ->whenBool('active', false) // ✅ false → 'false' ->whenBool('role', 'doctor'); // ❌ Non booléen // type:user|verified:true|active:false
Méthodes utilitaires pour tableaux et enums
Ces méthodes permettent d'ajouter facilement plusieurs paires à partir de tableaux ou d'enums.
withCases(string $prefix, array $values, string $suffix = ''): self
Ajoute des paires pour chaque valeur d'un tableau. Format : prefix_{value}{suffix}:true
$languages = ['fr', 'en', 'lu', 'ln']; $cluster = new ClusterVO('type:user') ->withCases('lang_', $languages); // type:user|lang_fr:true|lang_en:true|lang_lu:true|lang_ln:true
withEnum(string $prefix, string $enumClass, string $suffix = ''): self
Ajoute des paires pour chaque case d'un enum UnitEnum. Utilise le nom du case en minuscules.
$cluster = new ClusterVO('type:user') ->withEnum('role_', UserType::class); // type:user|role_patient:true|role_doctor:true|role_admin:true|role_staff:true
withEnumValues(string $prefix, string $enumClass, string $suffix = ''): self
Ajoute des paires pour les valeurs d'un enum. Pour BackedEnum, utilise $case->value. Pour UnitEnum, utilise $case->name. Tout est converti en minuscules.
$cluster = new ClusterVO('type:user') ->withEnumValues('status_', UserStatus::class); // type:user|status_active:true|status_inactive:true|status_pending:true|status_banned:true
Mode de recherche (AND / OR / NOT)
Le mode est utilisé UNIQUEMENT pour la recherche. Il est optionnel pour le stockage.
Définition du mode
// Sans mode (stockage) $cluster = new ClusterVO('type:user|status:active'); // Avec mode (recherche) $cluster = new ClusterVO('type:user|status:active@AND'); $cluster = new ClusterVO('type:user|status:active@OR'); $cluster = new ClusterVO('status:inactive@NOT');
Utilisation en recherche
// Mode AND : toutes les conditions doivent être remplies $cluster = new ClusterVO('type:user|status:active@AND'); // WHERE cluster LIKE '%type:user%' AND cluster LIKE '%status:active%' // Mode OR : au moins une condition doit être remplie $cluster = new ClusterVO('role_doctor:true|role_admin:true@OR'); // WHERE cluster LIKE '%role_doctor:true%' OR cluster LIKE '%role_admin:true%' // Mode NOT : aucune condition ne doit être remplie $cluster = new ClusterVO('status:inactive@NOT'); // WHERE cluster NOT LIKE '%status:inactive%'
Application à une requête
$cluster = new ClusterVO('type:user|status:active@AND'); $cluster->applyToQuery($query); // Ou avec une colonne personnalisée $cluster->applyToQuery($query, 'cluster');
⚠️ Important : applyToQuery() lève une exception si le cluster n'a pas de mode.
Collection de clusters (ClusterVOCollection)
La collection de clusters permet de combiner plusieurs clusters avec des opérateurs logiques.
use AndyDefer\LaravelIndexer\Collections\ClusterVOCollection; $clusters = new ClusterVOCollection(); $clusters->add(new ClusterVO('tenant:company_abc@AND')); $clusters->add(new ClusterVO('env:production@AND')); // Opérateur AND : tous les clusters doivent correspondre $results = $repository->findByClusters($clusters, 'AND'); // Opérateur OR : au moins un cluster doit correspondre $results = $repository->findByClusters($clusters, 'OR'); // Opérateur NOT : aucun cluster ne doit correspondre $results = $repository->findByClusters($clusters, 'NOT');
Filtrage avancé :
// Exemple : (role:doctor OR role:admin) AND status:active $clusters = new ClusterVOCollection(); $clusters->add(new ClusterVO('role_doctor:true|role_admin:true@OR')); $clusters->add(new ClusterVO('status:active@AND')); $results = $repository->findByClusters($clusters, 'AND');
Supprimer des valeurs
without(string $key): self
Supprime une clé entière.
$cluster = new ClusterVO('type:user|role:doctor|status:active'); // Supprime la clé $new = $cluster->without('role'); // type:user|status:active // Si la clé n'existe pas, retourne l'instance inchangée $new = $cluster->without('unknown'); // type:user|role:doctor|status:active (inchangé)
Vérification
hasAll(array $keys): bool
Vérifie si toutes les clés existent.
$cluster = new ClusterVO('type:user|role:doctor|status:active'); $cluster->hasAll(['type', 'role']); // true $cluster->hasAll(['type', 'role', 'status']); // true $cluster->hasAll(['type', 'unknown']); // false
hasAny(array $keys): bool
Vérifie si au moins une des clés existe.
$cluster = new ClusterVO('type:user|role:doctor'); $cluster->hasAny(['type', 'unknown']); // true $cluster->hasAny(['unknown', 'type']); // true $cluster->hasAny(['unknown1', 'unknown2']); // false
Méthodes statiques
make(string $key, string $value, ?string $mode = null): self
Crée un cluster avec une paire clé-valeur initiale.
// Stockage (sans mode) $cluster = ClusterVO::make('type', 'user') ->with('role', 'doctor') ->with('status', 'active'); // Recherche (avec mode) $cluster = ClusterVO::make('type', 'user', 'AND') ->with('role', 'doctor') ->with('status', 'active'); // type:user|role:doctor|status:active@AND
Conversion
getValue(): string
Retourne la chaîne brute du cluster.
$cluster = new ClusterVO('type:user|role:doctor'); $cluster->getValue(); // 'type:user|role:doctor'
__toString(): string
Alias de getValue().
$cluster = new ClusterVO('type:user|role:doctor'); echo $cluster; // 'type:user|role:doctor'
toArray(): array
Retourne le cluster sous forme de tableau.
$cluster = new ClusterVO('type:user|role:doctor'); $cluster->toArray(); // ['type' => 'user', 'role' => 'doctor']
Exemples avancés
Exemple 1: Modèle avec métadonnées
public function getIndexableCluster(): ClusterVO { return new ClusterVO('type:user') ->withTernary('status', (bool) $this->is_active, 'active', 'inactive') ->withTernary('verified', (bool) $this->email_verified_at, 'true', 'false') ->whenNotEmpty('role', $this->role) ->whenBool('verified', $this->email_verified_at !== null); } // type:user|status:active|verified:true|role:admin
Exemple 2: Modèle multi-tenant
public function getIndexableCluster(): ClusterVO { $cluster = new ClusterVO('type:doctor') ->with('tenant', $this->tenant_id) ->withTernary('status', $this->is_available, 'available', 'unavailable') ->whenNotEmpty('specialty', $this->specialty) ->whenNotEmpty('city', $this->city) ->whenNumeric('experience', $this->years_of_experience); if ($this->is_featured) { $cluster = $cluster->with('featured', 'true'); } return $cluster; } // tenant:company_abc|status:available|specialty:cardiology|city:Paris|experience:10|featured:true
Exemple 3: Utilisation des enums
public function getIndexableCluster(): ClusterVO { return new ClusterVO('type:user') ->withEnum('role_', UserType::class) ->withCases('lang_', $this->languages) ->withTernary('status', $this->is_active, 'active', 'inactive'); } // type:user|role_patient:true|role_doctor:true|lang_fr:true|lang_en:true|status:active
Exemple 4: Recherche avec cluster multiple (AND)
use AndyDefer\LaravelIndexer\Records\SearchQueryRecord; use AndyDefer\LaravelIndexer\ValueObjects\ClusterVO; use AndyDefer\LaravelIndexer\ValueObjects\SearchQueryVO; use AndyDefer\LaravelIndexer\Collections\ClusterVOCollection; // Recherche des médecins actifs en cardiologie à Paris $clusters = new ClusterVOCollection(); $clusters->add(new ClusterVO('type:doctor|specialty:cardiology@AND')); $clusters->add(new ClusterVO('city:Paris@AND')); $clusters->add(new ClusterVO('status:active@AND')); $query = new SearchQueryRecord( query: new SearchQueryVO('cardiologue=name,specialty'), clusters: $clusters, clustersOperator: 'AND' ); $results = $this->indexer->search($query);
Exemple 5: Recherche OR pour les rôles
$clusters = new ClusterVOCollection(); $clusters->add(new ClusterVO('role_doctor:true|role_admin:true@OR')); $query = new SearchQueryRecord( query: new SearchQueryVO('john=name'), clusters: $clusters, clustersOperator: 'OR' );
Exemple 6: Exclusion (NOT)
$clusters = new ClusterVOCollection(); $clusters->add(new ClusterVO('status:inactive@NOT')); $query = new SearchQueryRecord( query: new SearchQueryVO('john=name'), clusters: $clusters, clustersOperator: 'NOT' );
Indexer des données
Indexer un document
use AndyDefer\LaravelIndexer\Contracts\IndexerInterface; use AndyDefer\LaravelIndexer\Services\Composants\IndexableRecordFactory; class UserService { public function __construct( private IndexerInterface $indexer ) {} public function indexUser(User $user): void { $record = IndexableRecordFactory::convert($user); $this->indexer->index($record); } }
Indexer en masse
use AndyDefer\LaravelIndexer\Collections\IndexableRecordCollection; public function indexAllUsers(): void { $records = new IndexableRecordCollection; foreach (User::where('is_active', true)->cursor() as $user) { $records->add(IndexableRecordFactory::convert($user)); } $this->indexer->indexMany($records); }
Rafraîchir (mise à jour)
public function updateUser(User $user): void { $user->save(); $record = IndexableRecordFactory::convert($user); $this->indexer->refresh($record); }
GenericIndexerService
Service générique d'indexation qui fonctionne avec n'importe quel modèle Eloquent implémentant Indexable. Il gère automatiquement le chunking, le batch processing, la limitation et les opérations CRUD sur l'index.
Injection du service
use AndyDefer\LaravelIndexer\Contracts\GenericIndexerInterface; class UserIndexer { public function __construct( private readonly GenericIndexerInterface $genericIndexer, ) {} }
Indexer un document
// Avec le modèle directement $user = User::find($userId); $this->genericIndexer->index($user); // Ou avec la classe et l'ID $this->genericIndexer->indexById(User::class, $userId);
Indexer tous les documents
$this->genericIndexer->indexAll(User::class);
Indexer avec batch et limite
$this->genericIndexer ->setBatchSize(50) ->setLimit(1000) ->indexAll(Doctor::class);
Reconstruire tout l'index
$this->genericIndexer->reindexAll(User::class);
Supprimer un document
// Avec le modèle directement $user = User::find($userId); $this->genericIndexer->delete($user); // Ou avec la classe et l'ID $this->genericIndexer->deleteById(User::class, $userId);
Supprimer tous les documents d'un type
$this->genericIndexer->deleteAll(User::class);
Rafraîchir un document
// Avec le modèle directement $user = User::find($userId); $this->genericIndexer->refresh($user); // Ou avec la classe et l'ID $this->genericIndexer->refreshById(User::class, $userId);
Compter les documents indexés
$count = $this->genericIndexer->countIndexed(User::class);
Vérifier l'existence
// Avec le modèle directement $user = User::find($userId); if ($this->genericIndexer->exists($user)) { // L'utilisateur est indexé } // Ou avec la classe et l'ID if ($this->genericIndexer->existsById(User::class, $userId)) { // L'utilisateur est indexé }
Exemple complet
<?php declare(strict_types=1); use AndyDefer\LaravelIndexer\Contracts\GenericIndexerInterface; class DoctorIndexer { public function __construct( private readonly GenericIndexerInterface $genericIndexer, ) {} public function indexDoctor(int $doctorId): void { $doctor = Doctor::find($doctorId); if ($doctor) { $this->genericIndexer->index($doctor); } } public function reindexAllDoctors(): void { $this->genericIndexer ->setBatchSize(50) ->setLimit(10000) ->reindexAll(Doctor::class); } public function getIndexedDoctorCount(): int { return $this->genericIndexer->countIndexed(Doctor::class); } public function cleanupDoctorIndex(): void { $this->genericIndexer->deleteAll(Doctor::class); } }
GenericIndexModelsDirective
Directive CLI pour indexer les modèles configurés dans indexer.model_indexables.
Signature
index:models {batch=50} {limit=?} {models*} {--reindex} {--count} {--delete}
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 |
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 # Ignorer le batch (valeur par défaut) et appliquer une limite ./bin/directive index:models _ 20 [App.Models.User]
Configuration
// config/indexer.php 'model_indexables' => [ App\Models\User::class, App\Models\Hospital::class, App\Models\Specialty::class, ],
GenericOrchestratorRecurringTask
Tâche récurrente qui orchestre l'indexation de tous les modèles configurés. Elle récupère la liste des modèles depuis model_indexables et dispatch des tâches batch pour chaque lot.
Configuration
// config/indexer.php 'batch_size' => 50, 'model_indexables' => [ App\Models\User::class, App\Models\Hospital::class, ],
Fonctionnement
- Récupère les modèles configurables 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
Enregistrement
use AndyDefer\LaravelIndexer\Tasks\RecurringTasks\GenericOrchestratorRecurringTask; // La tâche est automatiquement enregistrée par le provider
GenericIndexBatchUniqueTask
Tâche unique qui indexe un lot d'éléments pour plusieurs modèles.
Payload
{
"items": [
{"modelClass": "App\\Models\\User", "id": 1},
{"modelClass": "App\\Models\\User", "id": 2},
{"modelClass": "App\\Models\\Hospital", "id": 3}
]
}
Fonctionnement
- Reçoit une
IndexableVOCollectioncontenant des paires(modelClass, id) - 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 (
getIndexableCluster())
Utilisation manuelle
use AndyDefer\LaravelIndexer\Collections\IndexableVOCollection; use AndyDefer\LaravelIndexer\Tasks\UniqueTasks\GenericIndexBatchUniqueTask; use AndyDefer\LaravelIndexer\ValueObjects\IndexableVO; use AndyDefer\Task\Records\UniqueTaskConfigRecord; use AndyDefer\Task\ValueObjects\DescriptionVO; use AndyDefer\Task\ValueObjects\DurationVO; use AndyDefer\Task\ValueObjects\Iso8601DateTimeVO; use AndyDefer\Task\ValueObjects\MaxFailedAttemptsVO; use AndyDefer\Task\ValueObjects\UniqueTaskFqcnVO; $collection = new IndexableVOCollection; $collection->add(new IndexableVO(User::class, 1)); $collection->add(new IndexableVO(User::class, 2)); $collection->add(new IndexableVO(Hospital::class, 3)); $payload = StrictDataObject::from([ 'items' => $collection, ]); $config = UniqueTaskConfigRecord::from([ 'scheduled_at' => new Iso8601DateTimeVO(now()->toIso8601String()), 'max_attempts' => new MaxFailedAttemptsVO(3), 'grace_period' => new DurationVO(3600), 'description' => new DescriptionVO('Batch task for indexing'), ]); $uniqueTaskService->register( new UniqueTaskFqcnVO(GenericIndexBatchUniqueTask::class), $payload, $config );
Rechercher
Comment fonctionne la recherche ?
- Le terme est normalisé (minuscules, accents supprimés)
- Le système génère tous les n-grammes 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)
Recherche simple avec clusters
use AndyDefer\LaravelIndexer\Records\SearchQueryRecord; use AndyDefer\LaravelIndexer\ValueObjects\SearchQueryVO; use AndyDefer\LaravelIndexer\Collections\ClusterVOCollection; public function searchUsers(string $query): array { $clusters = new ClusterVOCollection(); $searchQuery = new SearchQueryRecord( query: new SearchQueryVO($query . '=name,email,bio'), clusters: $clusters, clustersOperator: 'AND' ); $results = $this->indexer->search($searchQuery); $userIds = $results->getItems()->getIdValues()->toArray(); return User::whereIn('id', $userIds)->get(); }
Recherche multi-termes (AND)
$clusters = new ClusterVOCollection(); $query = new SearchQueryRecord( query: new SearchQueryVO('john=name|developer=bio'), clusters: $clusters, clustersOperator: 'AND' );
Recherche multi-champs (OR)
$clusters = new ClusterVOCollection(); $query = new SearchQueryRecord( query: new SearchQueryVO('john=name,email,bio'), clusters: $clusters, clustersOperator: 'AND' );
Recherche avec limite
$clusters = new ClusterVOCollection(); $query = new SearchQueryRecord( query: new SearchQueryVO('john=name,email'), clusters: $clusters, clustersOperator: 'AND', limit: 20 );
Filtrer par cluster AND
use AndyDefer\LaravelIndexer\Collections\ClusterVOCollection; use AndyDefer\LaravelIndexer\ValueObjects\ClusterVO; $clusters = new ClusterVOCollection(); $clusters->add(new ClusterVO('tenant:company_abc|type:user|status:active@AND')); $query = new SearchQueryRecord( query: new SearchQueryVO('john=name,email'), clusters: $clusters, clustersOperator: 'AND' ); $results = $this->indexer->search($query);
Filtrer par cluster OR
$clusters = new ClusterVOCollection(); $clusters->add(new ClusterVO('role_doctor:true@OR')); $clusters->add(new ClusterVO('role_admin:true@OR')); $query = new SearchQueryRecord( query: new SearchQueryVO('john=name,email'), clusters: $clusters, clustersOperator: 'OR' );
Filtrer par cluster NOT
$clusters = new ClusterVOCollection(); $clusters->add(new ClusterVO('status:inactive@NOT')); $query = new SearchQueryRecord( query: new SearchQueryVO('john=name,email'), clusters: $clusters, clustersOperator: 'NOT' );
Vérifier l'existence d'un document
use AndyDefer\LaravelIndexer\ValueObjects\IndexableFingerPrintVO; $fingerPrint = new IndexableFingerPrintVO('App.Models.User|123'); $exists = $this->indexer->exists($fingerPrint);
Autocomplétion
use AndyDefer\LaravelIndexer\Repositories\IndexedTokenRepository; class AutocompleteService { public function __construct( private IndexedTokenRepository $tokenRepository ) {} public function suggest(string $prefix): array { $tokens = $this->tokenRepository->autocomplete($prefix, 10); return $tokens->pluck('token')->toArray(); } }
Autocomplétion par champ
$tokens = $this->tokenRepository->getModel() ->newQuery() ->where('token', 'LIKE', $prefix . '%') ->where('field', 'name') ->select('token') ->distinct() ->limit(10) ->get();
Autocomplétion avec cluster
$cluster = new ClusterVO('tenant:company_abc@AND'); $tokens = $this->tokenRepository->getModel() ->newQuery() ->where('token', 'LIKE', $prefix . '%') ->whereHas('document', function ($q) use ($cluster) { $cluster->applyToQuery($q); }) ->select('token') ->distinct() ->limit(10) ->get();
Supprimer
Supprimer un document
use AndyDefer\LaravelIndexer\ValueObjects\IndexableFingerPrintVO; $fingerPrint = new IndexableFingerPrintVO('App.Models.User|123'); $this->indexer->delete($fingerPrint);
Supprimer plusieurs
use AndyDefer\LaravelIndexer\Collections\IndexableFingerPrintVOCollection; $collection = new IndexableFingerPrintVOCollection; $collection->add(new IndexableFingerPrintVO('App.Models.User|123')); $collection->add(new IndexableFingerPrintVO('App.Models.User|456')); $this->indexer->deleteMany($collection);
Supprimer par namespace
use AndyDefer\LaravelIndexer\Repositories\IndexedDocumentRepository; $repository = app(IndexedDocumentRepository::class); $repository->deleteByNamespace('App.Models.User');
Supprimer par cluster
$cluster = new ClusterVO('tenant:company_abc@AND'); $repository->deleteByCluster($cluster); $repository->deleteByClusterKeyValue('tenant', 'company_abc');
Supprimer par clusters multiples
$clusters = new ClusterVOCollection(); $clusters->add(new ClusterVO('tenant:company_abc@AND')); $clusters->add(new ClusterVO('env:production@AND')); $deleted = $repository->deleteByClusters($clusters, 'AND');
Vider l'index
$this->indexer->clear();
Repositories
IndexedDocumentRepository
use AndyDefer\LaravelIndexer\Repositories\IndexedDocumentRepository; use AndyDefer\LaravelIndexer\Collections\ClusterVOCollection; $repository = app(IndexedDocumentRepository::class); // Trouver $doc = $repository->findByFingerPrint($fingerPrint); $doc = $repository->findByFingerprintString('App.Models.User|123'); $docs = $repository->findByNamespace('App.Models.User'); $docs = $repository->findByCluster($cluster); $docs = $repository->findByClusters($clusters, 'AND'); $docs = $repository->findByClusters($clusters, 'OR'); $docs = $repository->findByClusters($clusters, 'NOT'); $docs = $repository->findByClusterKeyValue('tenant', 'company_abc'); $docs = $repository->findByIds(['uuid1', 'uuid2']); // Compter $count = $repository->countByNamespace('App.Models.User'); $count = $repository->countByCluster($cluster); $count = $repository->countByClusters($clusters, 'AND'); // Distinct $namespaces = $repository->getDistinctNamespaces(); $keys = $repository->getDistinctClusterKeys(); $values = $repository->getDistinctClusterValues('tenant'); // Vérifier $exists = $repository->existsByFingerPrint($fingerPrint); $exists = $repository->existsByNamespace('App.Models.User'); $exists = $repository->existsByCluster($cluster); $exists = $repository->existsByClusters($clusters, 'AND'); // Supprimer $repository->deleteByFingerPrint($fingerPrint); $repository->deleteByFingerprintString('App.Models.User|123'); $repository->deleteByNamespace('App.Models.User'); $repository->deleteByCluster($cluster); $repository->deleteByClusters($clusters, 'AND'); $repository->deleteByClusterKeyValue('tenant', 'company_abc');
IndexedTokenRepository
use AndyDefer\LaravelIndexer\Repositories\IndexedTokenRepository; use AndyDefer\LaravelIndexer\Enums\GramType; use AndyDefer\LaravelIndexer\Collections\ClusterVOCollection; $repository = app(IndexedTokenRepository::class); // Trouver $tokens = $repository->findByToken('john'); $tokens = $repository->findByType(GramType::LEXICAL); $tokens = $repository->findByField('name'); $tokens = $repository->findByDocumentId('uuid'); $tokens = $repository->findByDocumentFingerPrint($fingerPrint); $tokens = $repository->findByNamespace('App.Models.User'); $tokens = $repository->findByCluster($cluster); $tokens = $repository->findByClusters($clusters, 'AND'); $tokens = $repository->findByClusters($clusters, 'OR'); $tokens = $repository->findByClusters($clusters, 'NOT'); $tokens = $repository->findByClusterKeyValue('tenant', 'company_abc'); // Token + critères $tokens = $repository->findByTokenAndField('john', 'name'); $tokens = $repository->findByTokenAndType('john', GramType::LEXICAL); $tokens = $repository->findByTokenAndNamespace('john', 'App.Models.User'); $tokens = $repository->findByTokenAndCluster('john', $cluster); $tokens = $repository->findByTokenAndClusters('john', $clusters, 'AND'); $tokens = $repository->findByTokenFieldAndNamespace('john', 'name', 'App.Models.User'); // Document IDs par token $ids = $repository->getDocumentIdsForToken('john'); $ids = $repository->getDocumentIdsForTokenAndField('john', 'name'); $ids = $repository->getDocumentIdsForTokenAndCluster('john', $cluster); $ids = $repository->getDocumentIdsForTokenAndClusters('john', $clusters, 'AND'); $ids = $repository->getDocumentIdsForTokenFieldAndCluster('john', 'name', $cluster); $ids = $repository->getDocumentIdsForTokenFieldAndClusters('john', 'name', $clusters, 'AND'); // Compter $count = $repository->countDistinctTokens(); $count = $repository->countByType(GramType::LEXICAL); $count = $repository->countByField('name'); $count = $repository->countByNamespace('App.Models.User'); // Supprimer $repository->deleteByDocumentId('uuid'); $repository->deleteByDocumentFingerPrint($fingerPrint); $repository->deleteByNamespace('App.Models.User'); $repository->deleteByCluster($cluster); $repository->deleteByClusters($clusters, 'AND'); $repository->deleteByClusterKeyValue('tenant', 'company_abc'); $repository->deleteByToken('john'); $repository->deleteByTokenAndField('john', 'name');
Collections
IndexableSearchResultCollection
$results = $this->indexer->search($query); foreach ($results as $result) { $item = $result->item; $fingerprint = $item->fingerprint->getValue(); $field = $result->field; $gram = $result->gram_value; $type = $result->gram_type->value; } // Filtrage $byField = $results->filterByField('name'); $byNamespace = $results->filterByNamespace('App.Models.User'); // Extraction $ids = $results->getIds(); $items = $results->getItems(); $fingerPrints = $results->getFingerPrints(); // Groupement $byField = $results->groupByField(); $byNamespace = $results->groupByNamespace();
IndexableRecordCollection
$records = new IndexableRecordCollection; $records->add($record); $chunks = $records->chunk(100); $users = $records->filterByNamespace('App.Models.User'); $withTenant = $records->filterByCluster('tenant', 'company_abc'); $fingerPrints = $records->getFingerPrints(); $ids = $records->getIdValues(); $record = $records->findById('123'); $record = $records->findByIdAndNamespace('123', 'App.Models.User'); $hasId = $records->containsId('123'); $hasNamespace = $records->containsNamespace('App.Models.User'); $this->indexer->indexMany($records);
IndexableVOCollection
Collection spécialisée pour manipuler les IndexableVO.
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)); // Récupérer les IDs $ids = $collection->getIds(); // [1, 2, 3] // Récupérer les classes de modèles $classes = $collection->getModelClasses(); // [User::class, User::class, Hospital::class] // Récupérer les instances en UNE SEULE requête par classe $instances = $collection->getModelInstances(); // Filtrer par classe $users = $collection->filterByModelClass(User::class); // Filtrer par plusieurs classes $usersAndHospitals = $collection->filterByModelClasses([User::class, Hospital::class]); // Filtrer par classe et IDs $specificUsers = $collection->filterByModelClassAndIds(User::class, [1, 2]); // Vérification $hasId = $collection->containsId(1); $hasClass = $collection->containsModelClass(User::class); // Recherche $item = $collection->findById(1); // Groupement par classe $groups = $collection->groupByModelClass(); // [ // User::class => IndexableVOCollection [items: 2], // Hospital::class => IndexableVOCollection [items: 1], // ]
IndexableFingerPrintVOCollection
$fingerPrints = new IndexableFingerPrintVOCollection; $users = $fingerPrints->filterByNamespace('App.Models.User'); $ids = $fingerPrints->getIds(); $namespaces = $fingerPrints->getNamespaces(); $hasId = $fingerPrints->containsId('123'); $hasNamespace = $fingerPrints->containsNamespace('App.Models.User'); $fp = $fingerPrints->findByValue('App.Models.User|123'); $fp = $fingerPrints->findByIdAndNamespace('123', 'App.Models.User'); $grouped = $fingerPrints->groupByNamespace();
ClusterVOCollection
use AndyDefer\LaravelIndexer\Collections\ClusterVOCollection; $clusters = new ClusterVOCollection; $withTenant = $clusters->filterByKey('tenant'); $withSpecific = $clusters->filterByPair('tenant', 'company_abc'); $values = $clusters->getValuesForKey('tenant'); $keys = $clusters->getUniqueKeys(); $grouped = $clusters->groupByKey('tenant'); $hasKey = $clusters->hasKey('tenant'); $hasPair = $clusters->hasPair('tenant', 'company_abc'); $merged = $clusters->mergeAll();
License
MIT © Andy Defer