change de nom...
Expert Symfony certifié, Tech lead et DevOps (Azure / AWS / K8s / Terraform)
Cette session se concentre sur le développement d'un moteur de recherche centralisé pour un vaste catalogue de produits. Avec un volume impressionnant de 3 millions de produits répartis sur 290 fabricants, l’objectif est d’intégrer un moteur de recherche avancé au sein d'une API existante. Nous allons explorer les techniques et les outils pour améliorer la recherche multi-champs et multi-critères, tout en offrant des fonctionnalités avancées pour une expérience utilisateur optimale.
Le principal objectif de cette conférence est d’ajouter un moteur de recherche à l'API actuelle, permettant ainsi une recherche multi-champs et multi-critères. Les fonctionnalités visées incluent :
Fonctionnalités ciblées :
Les filtres proposés permettent de :
Les priorités d'affichages sont les suivantes :
La solution recommandée par Fabien consiste à remplacer le Data Provider natif d'API Platform, qui repose sur la bibliothèque elasticsearch/elasticsearch, par un nouvel ElasticaProvider personnalisé utilisant Elastica comme client Elasticsearch.
Le Data Provider proposé par Api Platform présente des limitations en matière de recherche avancée, notamment l'impossibilité d'intégrer l'API Count, car il ne prend en charge que l'API Search d'Elasticsearch. C'est pourquoi Fabien a opté pour Elastica, un package PHP qui offre une API flexible et performante, permettant d'exécuter des requêtes complexes de manière optimale.
Voici un exemple de requête utilisant Elastica :
$boolQuery = new Query\BoolQuery();
$boolQuery->addMust(new Query\Match('name', 'Casque Audio Pro'));
$boolQuery->addMust(new Query\Term(['category' => 'Électronique']));
// Filtre de plage de prix
$rangeFilter = new Query\Range('price', ['gte' => 100, 'lte' => 500]);
$boolQuery->addMust($rangeFilter);
// Définition de la requête principale
$query = new Query($boolQuery);
Cela permet d'interroger l'API Elasticsearch avec la requête suivante :
{
"query":{
"bool":{
"must":[
{
"match":{
"name":"Casque Audio Pro"
}
},
{
"term":{
"category":"Électronique"
}
},
{
"range":{
"price":{
"gte":100,
"lte":500
}
}
}
]
}
}
}
#[Entity(repositoryClass: ProductRepository::class)]
#[ApiResource(
operations: [
new GetCollection(
uriTemplate: '/products/search',
provider: ElasticaProvider::class,
stateOptions: new StateOptions(
indexName: 'products',
trackTotalHits: true,
)
),
new GetCollection(),
new Get(),
],
)]
#[ApiFilter(filterClass: ProductFilter::class)]
#[ApiFilter(filterClass: TermFilter::class, properties: ['brand' => 'brand'])]
#[ApiFilter(filterClass: RangeFilter::class, properties: ['price' => 'min_price'])]
#[ApiFilter(filterClass: CountFilter::class, properties: ['count'])]
class Product
Le Count Filter est une classe qui étend AbstractFilter et remplace l'API de recherche d'Elasticsearch pour retourner uniquement le nombre total de résultats d'une requête.
#[ApiFilter(filterClass: CountFilter::class, properties: ['count'])]
class Product
Application du Filtre : La méthode apply active le filtre de comptage dans le QueryBuilder. Elle vérifie l'existence du filtre count et, si présent et valide, renvoie le nombre total de résultats.
use App\ApiPlatform\Elasticsearch\QueryBuilder;
class CountFilter extends AbstractFilter
{
public function apply(QueryBuilder $queryBuilder, Operation $operation, array $uriVariables = [], array $context = []): void
{
if (!$this->filterExists('count')) {
return;
}
$value = filter_var($this->getRequestValue('count'), FILTER_VALIDATE_BOOLEAN);
if (!$value) {
return;
}
$queryBuilder->count(); ...
Les filtres personnalisés permettent des recherches complexes, adaptées aux besoins spécifiques. Voici un exemple de filtre :
Exemple de filtre : ProductFilter
class ProductFilter extends AbstractFilter
{
private const FILTER_NAME = 'query';
public function apply(QueryBuilder $queryBuilder, Operation $operation, array $uriVariables = [], array $context = []): void
{
if (!$this->filterExists(self::FILTER_NAME)) {
return;
}
$searchQuery = $this->getRequestValue(self::FILTER_NAME);
if ('' === $searchQuery) {
return;
}
$boolQuery = new BoolQuery();
$boolQuery->addShould(QueryHelper::matchExactly($searchQuery, 'code', 10));
$boolQuery->addShould(QueryHelper::matchByRegex($searchQuery, 'code', 2));
$boolQuery->addShould(QueryHelper::matchPartiallyOnMultipleFields($searchQuery, ['name^3', 'brand^2', 'model^1']));
$boolQuery->addShould(QueryHelper::matchPartiallyWithFaultTolerance($searchQuery, ['name', 'brand', 'model']));
$queryBuilder->getQuery()->addMust($boolQuery);
}
}
Voici un exemple de filtre pour les produits publics ou privés, basé sur company_id :
class CompanyFilter extends AbstractFilter
{
public function apply(QueryBuilder $queryBuilder, Operation $operation, array $uriVariables = [], array $context = []): void
{
$user = $this->getUser();
$company = $user->getCompany();
$boolQuery = new BoolQuery();
if ($company) {
$boolQuery->addShould((new Term())->setTerm('company_id', $company->getId()));
}
$boolQuery->addShould((new BoolQuery())->addMustNot(['exists' => ['field' => 'company_id']]));
$queryBuilder->getQuery()->addFilter($boolQuery);
}
}
L'adoption d'Elastica dans le développement du moteur de recherche a permis de concevoir un système à la fois robuste et flexible, capable de gérer efficacement un volume important de données tout en intégrant des fonctionnalités avancées. Cette stratégie renforce non seulement la performance et la maintenabilité du code, mais favorise également une intégration optimisée avec Symfony. |
Pour plus de détails, vous pouvez consulter le code source de la démonstration : Elastica Provider Demo.