Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multisearch + dynamic filters V2 #22

Merged
merged 16 commits into from
Sep 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions src/Document/Metadata/Facet.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@

final class Facet
{
public function __construct(public readonly string $name)
{
public function __construct(
public readonly string $name,
public readonly string $type,
) {
}
}
11 changes: 10 additions & 1 deletion src/Document/Metadata/Metadata.php
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ private function loadAttributes(\ReflectionProperty|\ReflectionMethod $attribute
}

if ($attribute instanceof FacetAttribute) {
$this->facetableAttributes[$name] = new Facet($name);
$this->facetableAttributes[$name] = new Facet($name, self::getFacetType($attributesAware));
}

if ($attribute instanceof SearchableAttribute) {
Expand Down Expand Up @@ -167,4 +167,13 @@ private static function resolveName(\ReflectionProperty|\ReflectionMethod $refle

return null;
}

private static function getFacetType(\ReflectionProperty|\ReflectionMethod $attributesAware): string
{
if ($attributesAware instanceof \ReflectionProperty) {
return str_replace('?', '', (string) $attributesAware->getType());
}

return str_replace('?', '', (string) $attributesAware->getReturnType());
}
}
59 changes: 43 additions & 16 deletions src/Engine/SearchEngine.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,41 +5,68 @@
namespace Setono\SyliusMeilisearchPlugin\Engine;

use Meilisearch\Client;
use Meilisearch\Contracts\SearchQuery;
use Meilisearch\Search\SearchResult;
use Setono\SyliusMeilisearchPlugin\Config\Index;
use Setono\SyliusMeilisearchPlugin\Document\Metadata\MetadataFactoryInterface;
use Setono\SyliusMeilisearchPlugin\Meilisearch\Builder\FilterBuilderInterface;
use Setono\SyliusMeilisearchPlugin\Meilisearch\Filter\FilterBuilderInterface;
use Setono\SyliusMeilisearchPlugin\Meilisearch\Query\MainQueryBuilderInterface;
use Setono\SyliusMeilisearchPlugin\Meilisearch\Query\SubQueriesBuilderInterface;
use Setono\SyliusMeilisearchPlugin\Resolver\IndexName\IndexNameResolverInterface;

final class SearchEngine implements SearchEngineInterface
{
public function __construct(

Check failure on line 19 in src/Engine/SearchEngine.php

View workflow job for this annotation

GitHub Actions / Backwards Compatibility Check

The number of required arguments for Setono\SyliusMeilisearchPlugin\Engine\SearchEngine#__construct() increased from 6 to 7

Check failure on line 19 in src/Engine/SearchEngine.php

View workflow job for this annotation

GitHub Actions / Backwards Compatibility Check

The parameter $filterBuilder of Setono\SyliusMeilisearchPlugin\Engine\SearchEngine#__construct() changed from Setono\SyliusMeilisearchPlugin\Meilisearch\Builder\FilterBuilderInterface to a non-contravariant Setono\SyliusMeilisearchPlugin\Meilisearch\Filter\FilterBuilderInterface

Check failure on line 19 in src/Engine/SearchEngine.php

View workflow job for this annotation

GitHub Actions / Backwards Compatibility Check

The parameter $hitsPerPage of Setono\SyliusMeilisearchPlugin\Engine\SearchEngine#__construct() changed from int to a non-contravariant Setono\SyliusMeilisearchPlugin\Meilisearch\Query\MainQueryBuilderInterface
private readonly MetadataFactoryInterface $metadataFactory,
private readonly FilterBuilderInterface $filterBuilder,
private readonly Index $index,
private readonly IndexNameResolverInterface $indexNameResolver,
private readonly Client $client,
private readonly int $hitsPerPage,
private readonly MainQueryBuilderInterface $mainQueryBuilder,
private readonly SubQueriesBuilderInterface $subQueriesBuilder,
) {
}

public function execute(?string $query, array $parameters = []): SearchResult
{
$page = max(1, (int) ($parameters['p'] ?? 1));
$sort = (string) ($parameters['sort'] ?? '');

$indexName = $this->indexNameResolver->resolve($this->index);
$metadata = $this->metadataFactory->getMetadataFor($this->index->document);
$facetsNames = $metadata->getFacetableAttributeNames();
$facets = $metadata->getFacetableAttributes();

/** @var array<string, mixed> $facetsFilter */
$facetsFilter = (array) ($parameters['facets'] ?? []);
/** @var array<string, mixed> $filters */
$filters = $this->filterBuilder->build($facets, $facetsFilter);

$mainQuery = $this->mainQueryBuilder->build(
$indexName,
$query ?? '',
$facetsNames,
$filters,
max(1, (int) ($parameters['p'] ?? 1)),
(string) ($parameters['sort'] ?? ''),
);

/** @var list<SearchQuery> $queries */
$queries = array_merge(
[$mainQuery],
$this->subQueriesBuilder->build($indexName, $query ?? '', $facets, $facetsFilter),
);

/** @var array<SearchResult> $results */
$results = $this->client->multiSearch($queries)['results'] ?? [];

return $this->provideSearchResult($results);
}

private function provideSearchResult(array $results): SearchResult
{
/** @var array{facetDistribution: array<string, int>} $firstResult */
$firstResult = current($results);
/** @psalm-suppress MixedArgument (just for now) */
$firstResult['facetDistribution'] = array_merge(...array_column($results, 'facetDistribution'));

$searchParams = [
'facets' => $metadata->getFacetableAttributeNames(),
'filter' => $this->filterBuilder->build($parameters),
'hitsPerPage' => $this->hitsPerPage,
'page' => $page,
];
if ('' !== $sort) {
$searchParams['sort'] = [$sort];
}

return $this->client->index($this->indexNameResolver->resolve($this->index))->search($query, $searchParams);
return new SearchResult($firstResult);
}
}
13 changes: 6 additions & 7 deletions src/Form/Builder/CheckboxFacetFormBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,17 @@

namespace Setono\SyliusMeilisearchPlugin\Form\Builder;

use Setono\SyliusMeilisearchPlugin\Document\Metadata\Facet;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\FormBuilderInterface;
use function Symfony\Component\String\u;

final class CheckboxFacetFormBuilder implements FacetFormBuilderInterface
{
public function build(FormBuilderInterface $builder, string $name, array $values, array $stats = null): void
public function build(FormBuilderInterface $builder, Facet $facet, array $values, array $stats = null): void

Check failure on line 14 in src/Form/Builder/CheckboxFacetFormBuilder.php

View workflow job for this annotation

GitHub Actions / Backwards Compatibility Check

The parameter $name of Setono\SyliusMeilisearchPlugin\Form\Builder\CheckboxFacetFormBuilder#build() changed from string to a non-contravariant Setono\SyliusMeilisearchPlugin\Document\Metadata\Facet
{
$builder->add($name, CheckboxType::class, [
'label' => sprintf('setono_sylius_meilisearch.form.search.facet.%s', u($name)->snake()),
$builder->add($facet->name, CheckboxType::class, [
'label' => sprintf('setono_sylius_meilisearch.form.search.facet.%s', u($facet->name)->snake()),
'label_translation_parameters' => [
'%count%' => $values['true'],
],
Expand All @@ -22,11 +23,9 @@
]);
}

public function supports(string $name, array $values, array $stats = null): bool
public function supports(Facet $facet, array $values, array $stats = null): bool

Check failure on line 26 in src/Form/Builder/CheckboxFacetFormBuilder.php

View workflow job for this annotation

GitHub Actions / Backwards Compatibility Check

The parameter $name of Setono\SyliusMeilisearchPlugin\Form\Builder\CheckboxFacetFormBuilder#supports() changed from string to a non-contravariant Setono\SyliusMeilisearchPlugin\Document\Metadata\Facet
{
$c = count($values);

return match ($c) {
return $facet->type === 'bool' && match (count($values)) {
1 => isset($values['true']),
2 => isset($values['true'], $values['false']),
default => false,
Expand Down
13 changes: 9 additions & 4 deletions src/Form/Builder/ChoiceFacetFormBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,20 @@

namespace Setono\SyliusMeilisearchPlugin\Form\Builder;

use Setono\SyliusMeilisearchPlugin\Document\Metadata\Facet;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
use function Symfony\Component\String\u;

final class ChoiceFacetFormBuilder implements FacetFormBuilderInterface
{
public function build(FormBuilderInterface $builder, string $name, array $values, array $stats = null): void
public function build(FormBuilderInterface $builder, Facet $facet, array $values, array $stats = null): void

Check failure on line 14 in src/Form/Builder/ChoiceFacetFormBuilder.php

View workflow job for this annotation

GitHub Actions / Backwards Compatibility Check

The parameter $name of Setono\SyliusMeilisearchPlugin\Form\Builder\ChoiceFacetFormBuilder#build() changed from string to a non-contravariant Setono\SyliusMeilisearchPlugin\Document\Metadata\Facet
{
$keys = array_keys($values);
$choices = array_combine($keys, $keys);

$builder->add($name, ChoiceType::class, [
'label' => sprintf('setono_sylius_meilisearch.form.search.facet.%s', u($name)->snake()),
$builder->add($facet->name, ChoiceType::class, [
'label' => sprintf('setono_sylius_meilisearch.form.search.facet.%s', u($facet->name)->snake()),
'choices' => $choices,
'choice_label' => fn (string $key) => sprintf('%s (%d)', $key, $values[$key]),
'expanded' => true,
Expand All @@ -26,8 +27,12 @@
]);
}

public function supports(string $name, array $values, array $stats = null): bool
public function supports(Facet $facet, array $values, array $stats = null): bool

Check failure on line 30 in src/Form/Builder/ChoiceFacetFormBuilder.php

View workflow job for this annotation

GitHub Actions / Backwards Compatibility Check

The parameter $name of Setono\SyliusMeilisearchPlugin\Form\Builder\ChoiceFacetFormBuilder#supports() changed from string to a non-contravariant Setono\SyliusMeilisearchPlugin\Document\Metadata\Facet
{
if ($facet->type !== 'array') {
return false;
}

$keys = array_keys($values);
if (count($keys) < 2) {
return false;
Expand Down
13 changes: 7 additions & 6 deletions src/Form/Builder/CompositeFacetFormBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,30 +5,31 @@
namespace Setono\SyliusMeilisearchPlugin\Form\Builder;

use Setono\CompositeCompilerPass\CompositeService;
use Setono\SyliusMeilisearchPlugin\Document\Metadata\Facet;
use Symfony\Component\Form\FormBuilderInterface;

/**
* @extends CompositeService<FacetFormBuilderInterface>
*/
final class CompositeFacetFormBuilder extends CompositeService implements FacetFormBuilderInterface
{
public function build(FormBuilderInterface $builder, string $name, array $values, array $stats = null): void
public function build(FormBuilderInterface $builder, Facet $facet, array $values, array $stats = null): void

Check failure on line 16 in src/Form/Builder/CompositeFacetFormBuilder.php

View workflow job for this annotation

GitHub Actions / Backwards Compatibility Check

The parameter $name of Setono\SyliusMeilisearchPlugin\Form\Builder\CompositeFacetFormBuilder#build() changed from string to a non-contravariant Setono\SyliusMeilisearchPlugin\Document\Metadata\Facet
{
foreach ($this->services as $service) {
if ($service->supports($name, $values, $stats)) {
$service->build($builder, $name, $values, $stats);
if ($service->supports($facet, $values, $stats)) {
$service->build($builder, $facet, $values, $stats);

return;
}
}

throw new \RuntimeException(sprintf('No facet form builder supports the facet with name "%s"', $name));
throw new \RuntimeException(sprintf('No facet form builder supports the facet with name "%s"', $facet->name));
}

public function supports(string $name, array $values, array $stats = null): bool
public function supports(Facet $facet, array $values, array $stats = null): bool

Check failure on line 29 in src/Form/Builder/CompositeFacetFormBuilder.php

View workflow job for this annotation

GitHub Actions / Backwards Compatibility Check

The parameter $name of Setono\SyliusMeilisearchPlugin\Form\Builder\CompositeFacetFormBuilder#supports() changed from string to a non-contravariant Setono\SyliusMeilisearchPlugin\Document\Metadata\Facet
{
foreach ($this->services as $service) {
if ($service->supports($name, $values, $stats)) {
if ($service->supports($facet, $values, $stats)) {
return true;
}
}
Expand Down
7 changes: 3 additions & 4 deletions src/Form/Builder/FacetFormBuilderInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,20 @@

namespace Setono\SyliusMeilisearchPlugin\Form\Builder;

use Setono\SyliusMeilisearchPlugin\Document\Metadata\Facet;
use Symfony\Component\Form\FormBuilderInterface;

interface FacetFormBuilderInterface
{
/**
* @param string $name The name of the facet. This could be 'price' or 'color' for instance
* @param array<string, int> $values The values of the facet. This could be ['red' => 10, 'blue' => 5] where the key is the facet value and the value is the number of matching documents
* @param array{min: int|float, max: int|float}|null $stats The stats of the facet. This could be ['min' => 10, 'max' => 100] where min is the minimum value and max is the maximum value
*/
public function build(FormBuilderInterface $builder, string $name, array $values, array $stats = null): void;
public function build(FormBuilderInterface $builder, Facet $facet, array $values, array $stats = null): void;

Check failure on line 16 in src/Form/Builder/FacetFormBuilderInterface.php

View workflow job for this annotation

GitHub Actions / Backwards Compatibility Check

The parameter $name of Setono\SyliusMeilisearchPlugin\Form\Builder\FacetFormBuilderInterface#build() changed from string to a non-contravariant Setono\SyliusMeilisearchPlugin\Document\Metadata\Facet

/**
* @param string $name The name of the facet. This could be 'price' or 'color' for instance
* @param array<string, int> $values
* @param array{min: int|float, max: int|float}|null $stats
*/
public function supports(string $name, array $values, array $stats = null): bool;
public function supports(Facet $facet, array $values, array $stats = null): bool;
}
31 changes: 31 additions & 0 deletions src/Form/Builder/RangeFacetFormBuilder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

declare(strict_types=1);

namespace Setono\SyliusMeilisearchPlugin\Form\Builder;

use Setono\SyliusMeilisearchPlugin\Document\Metadata\Facet;
use Setono\SyliusMeilisearchPlugin\Form\Type\RangeType;
use Symfony\Component\Form\FormBuilderInterface;
use function Symfony\Component\String\u;

final class RangeFacetFormBuilder implements FacetFormBuilderInterface
{
public function build(FormBuilderInterface $builder, Facet $facet, array $values, array $stats = null): void
{
if ($stats === null || !isset($stats['min']) && !isset($stats['max'])) {
return;
}

$builder->add($facet->name, RangeType::class, [
'label' => sprintf('setono_sylius_meilisearch.form.search.facet.%s', u($facet->name)->snake()),
'required' => false,
'block_prefix' => 'setono_sylius_meilisearch_facet_range',
]);
}

public function supports(Facet $facet, array $values, array $stats = null): bool
{
return $facet->type === 'float';
}
}
12 changes: 10 additions & 2 deletions src/Form/Builder/SearchFormBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
namespace Setono\SyliusMeilisearchPlugin\Form\Builder;

use Meilisearch\Search\SearchResult;
use Setono\SyliusMeilisearchPlugin\Config\Index;
use Setono\SyliusMeilisearchPlugin\Document\Metadata\MetadataFactoryInterface;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Component\Form\FormBuilderInterface;
Expand All @@ -16,11 +18,15 @@ final class SearchFormBuilder implements SearchFormBuilderInterface
public function __construct(
private readonly FormFactoryInterface $formFactory,
private readonly FacetFormBuilderInterface $facetFormBuilder,
private readonly MetadataFactoryInterface $metadataFactory,
private readonly Index $index,
) {
}

public function build(SearchResult $searchResult): FormInterface
{
$metadata = $this->metadataFactory->getMetadataFor($this->index->document);

$searchFormBuilder = $this
->formFactory
->createNamedBuilder('', options: [
Expand Down Expand Up @@ -49,6 +55,8 @@ public function build(SearchResult $searchResult): FormInterface
*/
$facetStats = $searchResult->getFacetStats();

$facets = $metadata->getFacetableAttributes();

/**
* Here is an example of the facet distribution array
*
Expand All @@ -70,8 +78,8 @@ public function build(SearchResult $searchResult): FormInterface
* @var array<string, int> $values
*/
foreach ($searchResult->getFacetDistribution() as $name => $values) {
if ($this->facetFormBuilder->supports($name, $values, $facetStats[$name] ?? null)) {
$this->facetFormBuilder->build($facetsFormBuilder, $name, $values, $facetStats[$name] ?? null);
if ($this->facetFormBuilder->supports($facets[$name], $values, $facetStats[$name] ?? null)) {
$this->facetFormBuilder->build($facetsFormBuilder, $facets[$name], $values, $facetStats[$name] ?? null);
}
}

Expand Down
29 changes: 29 additions & 0 deletions src/Form/Type/RangeType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

declare(strict_types=1);

namespace Setono\SyliusMeilisearchPlugin\Form\Type;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\NumberType;
use Symfony\Component\Form\FormBuilderInterface;

final class RangeType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('min', NumberType::class, [
'label' => 'Min',
])
->add('max', NumberType::class, [
'label' => 'Max',
])
;
}

public function getBlockPrefix(): string
{
return 'setono_sylius_meilisearch_range';
}
}
29 changes: 0 additions & 29 deletions src/Meilisearch/Builder/FilterBuilder.php

This file was deleted.

Loading
Loading