Skip to content

Commit e505ddf

Browse files
authored
Merge pull request #22 from Setono/multisearch-v2
Multisearch + dynamic filters V2
2 parents fd8711b + c942289 commit e505ddf

32 files changed

+547
-86
lines changed

src/Document/Metadata/Facet.php

+4-2
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66

77
final class Facet
88
{
9-
public function __construct(public readonly string $name)
10-
{
9+
public function __construct(
10+
public readonly string $name,
11+
public readonly string $type,
12+
) {
1113
}
1214
}

src/Document/Metadata/Metadata.php

+10-1
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ private function loadAttributes(\ReflectionProperty|\ReflectionMethod $attribute
7272
}
7373

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

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

168168
return null;
169169
}
170+
171+
private static function getFacetType(\ReflectionProperty|\ReflectionMethod $attributesAware): string
172+
{
173+
if ($attributesAware instanceof \ReflectionProperty) {
174+
return str_replace('?', '', (string) $attributesAware->getType());
175+
}
176+
177+
return str_replace('?', '', (string) $attributesAware->getReturnType());
178+
}
170179
}

src/Engine/SearchEngine.php

+43-16
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,13 @@
55
namespace Setono\SyliusMeilisearchPlugin\Engine;
66

77
use Meilisearch\Client;
8+
use Meilisearch\Contracts\SearchQuery;
89
use Meilisearch\Search\SearchResult;
910
use Setono\SyliusMeilisearchPlugin\Config\Index;
1011
use Setono\SyliusMeilisearchPlugin\Document\Metadata\MetadataFactoryInterface;
11-
use Setono\SyliusMeilisearchPlugin\Meilisearch\Builder\FilterBuilderInterface;
12+
use Setono\SyliusMeilisearchPlugin\Meilisearch\Filter\FilterBuilderInterface;
13+
use Setono\SyliusMeilisearchPlugin\Meilisearch\Query\MainQueryBuilderInterface;
14+
use Setono\SyliusMeilisearchPlugin\Meilisearch\Query\SubQueriesBuilderInterface;
1215
use Setono\SyliusMeilisearchPlugin\Resolver\IndexName\IndexNameResolverInterface;
1316

1417
final class SearchEngine implements SearchEngineInterface
@@ -19,27 +22,51 @@ public function __construct(
1922
private readonly Index $index,
2023
private readonly IndexNameResolverInterface $indexNameResolver,
2124
private readonly Client $client,
22-
private readonly int $hitsPerPage,
25+
private readonly MainQueryBuilderInterface $mainQueryBuilder,
26+
private readonly SubQueriesBuilderInterface $subQueriesBuilder,
2327
) {
2428
}
2529

2630
public function execute(?string $query, array $parameters = []): SearchResult
2731
{
28-
$page = max(1, (int) ($parameters['p'] ?? 1));
29-
$sort = (string) ($parameters['sort'] ?? '');
30-
32+
$indexName = $this->indexNameResolver->resolve($this->index);
3133
$metadata = $this->metadataFactory->getMetadataFor($this->index->document);
34+
$facetsNames = $metadata->getFacetableAttributeNames();
35+
$facets = $metadata->getFacetableAttributes();
36+
37+
/** @var array<string, mixed> $facetsFilter */
38+
$facetsFilter = (array) ($parameters['facets'] ?? []);
39+
/** @var array<string, mixed> $filters */
40+
$filters = $this->filterBuilder->build($facets, $facetsFilter);
41+
42+
$mainQuery = $this->mainQueryBuilder->build(
43+
$indexName,
44+
$query ?? '',
45+
$facetsNames,
46+
$filters,
47+
max(1, (int) ($parameters['p'] ?? 1)),
48+
(string) ($parameters['sort'] ?? ''),
49+
);
50+
51+
/** @var list<SearchQuery> $queries */
52+
$queries = array_merge(
53+
[$mainQuery],
54+
$this->subQueriesBuilder->build($indexName, $query ?? '', $facets, $facetsFilter),
55+
);
56+
57+
/** @var array<SearchResult> $results */
58+
$results = $this->client->multiSearch($queries)['results'] ?? [];
59+
60+
return $this->provideSearchResult($results);
61+
}
62+
63+
private function provideSearchResult(array $results): SearchResult
64+
{
65+
/** @var array{facetDistribution: array<string, int>} $firstResult */
66+
$firstResult = current($results);
67+
/** @psalm-suppress MixedArgument (just for now) */
68+
$firstResult['facetDistribution'] = array_merge(...array_column($results, 'facetDistribution'));
3269

33-
$searchParams = [
34-
'facets' => $metadata->getFacetableAttributeNames(),
35-
'filter' => $this->filterBuilder->build($parameters),
36-
'hitsPerPage' => $this->hitsPerPage,
37-
'page' => $page,
38-
];
39-
if ('' !== $sort) {
40-
$searchParams['sort'] = [$sort];
41-
}
42-
43-
return $this->client->index($this->indexNameResolver->resolve($this->index))->search($query, $searchParams);
70+
return new SearchResult($firstResult);
4471
}
4572
}

src/Form/Builder/CheckboxFacetFormBuilder.php

+6-7
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,17 @@
44

55
namespace Setono\SyliusMeilisearchPlugin\Form\Builder;
66

7+
use Setono\SyliusMeilisearchPlugin\Document\Metadata\Facet;
78
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
89
use Symfony\Component\Form\FormBuilderInterface;
910
use function Symfony\Component\String\u;
1011

1112
final class CheckboxFacetFormBuilder implements FacetFormBuilderInterface
1213
{
13-
public function build(FormBuilderInterface $builder, string $name, array $values, array $stats = null): void
14+
public function build(FormBuilderInterface $builder, Facet $facet, array $values, array $stats = null): void
1415
{
15-
$builder->add($name, CheckboxType::class, [
16-
'label' => sprintf('setono_sylius_meilisearch.form.search.facet.%s', u($name)->snake()),
16+
$builder->add($facet->name, CheckboxType::class, [
17+
'label' => sprintf('setono_sylius_meilisearch.form.search.facet.%s', u($facet->name)->snake()),
1718
'label_translation_parameters' => [
1819
'%count%' => $values['true'],
1920
],
@@ -22,11 +23,9 @@ public function build(FormBuilderInterface $builder, string $name, array $values
2223
]);
2324
}
2425

25-
public function supports(string $name, array $values, array $stats = null): bool
26+
public function supports(Facet $facet, array $values, array $stats = null): bool
2627
{
27-
$c = count($values);
28-
29-
return match ($c) {
28+
return $facet->type === 'bool' && match (count($values)) {
3029
1 => isset($values['true']),
3130
2 => isset($values['true'], $values['false']),
3231
default => false,

src/Form/Builder/ChoiceFacetFormBuilder.php

+9-4
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,20 @@
44

55
namespace Setono\SyliusMeilisearchPlugin\Form\Builder;
66

7+
use Setono\SyliusMeilisearchPlugin\Document\Metadata\Facet;
78
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
89
use Symfony\Component\Form\FormBuilderInterface;
910
use function Symfony\Component\String\u;
1011

1112
final class ChoiceFacetFormBuilder implements FacetFormBuilderInterface
1213
{
13-
public function build(FormBuilderInterface $builder, string $name, array $values, array $stats = null): void
14+
public function build(FormBuilderInterface $builder, Facet $facet, array $values, array $stats = null): void
1415
{
1516
$keys = array_keys($values);
1617
$choices = array_combine($keys, $keys);
1718

18-
$builder->add($name, ChoiceType::class, [
19-
'label' => sprintf('setono_sylius_meilisearch.form.search.facet.%s', u($name)->snake()),
19+
$builder->add($facet->name, ChoiceType::class, [
20+
'label' => sprintf('setono_sylius_meilisearch.form.search.facet.%s', u($facet->name)->snake()),
2021
'choices' => $choices,
2122
'choice_label' => fn (string $key) => sprintf('%s (%d)', $key, $values[$key]),
2223
'expanded' => true,
@@ -26,8 +27,12 @@ public function build(FormBuilderInterface $builder, string $name, array $values
2627
]);
2728
}
2829

29-
public function supports(string $name, array $values, array $stats = null): bool
30+
public function supports(Facet $facet, array $values, array $stats = null): bool
3031
{
32+
if ($facet->type !== 'array') {
33+
return false;
34+
}
35+
3136
$keys = array_keys($values);
3237
if (count($keys) < 2) {
3338
return false;

src/Form/Builder/CompositeFacetFormBuilder.php

+7-6
Original file line numberDiff line numberDiff line change
@@ -5,30 +5,31 @@
55
namespace Setono\SyliusMeilisearchPlugin\Form\Builder;
66

77
use Setono\CompositeCompilerPass\CompositeService;
8+
use Setono\SyliusMeilisearchPlugin\Document\Metadata\Facet;
89
use Symfony\Component\Form\FormBuilderInterface;
910

1011
/**
1112
* @extends CompositeService<FacetFormBuilderInterface>
1213
*/
1314
final class CompositeFacetFormBuilder extends CompositeService implements FacetFormBuilderInterface
1415
{
15-
public function build(FormBuilderInterface $builder, string $name, array $values, array $stats = null): void
16+
public function build(FormBuilderInterface $builder, Facet $facet, array $values, array $stats = null): void
1617
{
1718
foreach ($this->services as $service) {
18-
if ($service->supports($name, $values, $stats)) {
19-
$service->build($builder, $name, $values, $stats);
19+
if ($service->supports($facet, $values, $stats)) {
20+
$service->build($builder, $facet, $values, $stats);
2021

2122
return;
2223
}
2324
}
2425

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

28-
public function supports(string $name, array $values, array $stats = null): bool
29+
public function supports(Facet $facet, array $values, array $stats = null): bool
2930
{
3031
foreach ($this->services as $service) {
31-
if ($service->supports($name, $values, $stats)) {
32+
if ($service->supports($facet, $values, $stats)) {
3233
return true;
3334
}
3435
}

src/Form/Builder/FacetFormBuilderInterface.php

+3-4
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,20 @@
44

55
namespace Setono\SyliusMeilisearchPlugin\Form\Builder;
66

7+
use Setono\SyliusMeilisearchPlugin\Document\Metadata\Facet;
78
use Symfony\Component\Form\FormBuilderInterface;
89

910
interface FacetFormBuilderInterface
1011
{
1112
/**
12-
* @param string $name The name of the facet. This could be 'price' or 'color' for instance
1313
* @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
1414
* @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
1515
*/
16-
public function build(FormBuilderInterface $builder, string $name, array $values, array $stats = null): void;
16+
public function build(FormBuilderInterface $builder, Facet $facet, array $values, array $stats = null): void;
1717

1818
/**
19-
* @param string $name The name of the facet. This could be 'price' or 'color' for instance
2019
* @param array<string, int> $values
2120
* @param array{min: int|float, max: int|float}|null $stats
2221
*/
23-
public function supports(string $name, array $values, array $stats = null): bool;
22+
public function supports(Facet $facet, array $values, array $stats = null): bool;
2423
}
+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Setono\SyliusMeilisearchPlugin\Form\Builder;
6+
7+
use Setono\SyliusMeilisearchPlugin\Document\Metadata\Facet;
8+
use Setono\SyliusMeilisearchPlugin\Form\Type\RangeType;
9+
use Symfony\Component\Form\FormBuilderInterface;
10+
use function Symfony\Component\String\u;
11+
12+
final class RangeFacetFormBuilder implements FacetFormBuilderInterface
13+
{
14+
public function build(FormBuilderInterface $builder, Facet $facet, array $values, array $stats = null): void
15+
{
16+
if ($stats === null || !isset($stats['min']) && !isset($stats['max'])) {
17+
return;
18+
}
19+
20+
$builder->add($facet->name, RangeType::class, [
21+
'label' => sprintf('setono_sylius_meilisearch.form.search.facet.%s', u($facet->name)->snake()),
22+
'required' => false,
23+
'block_prefix' => 'setono_sylius_meilisearch_facet_range',
24+
]);
25+
}
26+
27+
public function supports(Facet $facet, array $values, array $stats = null): bool
28+
{
29+
return $facet->type === 'float';
30+
}
31+
}

src/Form/Builder/SearchFormBuilder.php

+10-2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
namespace Setono\SyliusMeilisearchPlugin\Form\Builder;
66

77
use Meilisearch\Search\SearchResult;
8+
use Setono\SyliusMeilisearchPlugin\Config\Index;
9+
use Setono\SyliusMeilisearchPlugin\Document\Metadata\MetadataFactoryInterface;
810
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
911
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
1012
use Symfony\Component\Form\FormBuilderInterface;
@@ -16,11 +18,15 @@ final class SearchFormBuilder implements SearchFormBuilderInterface
1618
public function __construct(
1719
private readonly FormFactoryInterface $formFactory,
1820
private readonly FacetFormBuilderInterface $facetFormBuilder,
21+
private readonly MetadataFactoryInterface $metadataFactory,
22+
private readonly Index $index,
1923
) {
2024
}
2125

2226
public function build(SearchResult $searchResult): FormInterface
2327
{
28+
$metadata = $this->metadataFactory->getMetadataFor($this->index->document);
29+
2430
$searchFormBuilder = $this
2531
->formFactory
2632
->createNamedBuilder('', options: [
@@ -49,6 +55,8 @@ public function build(SearchResult $searchResult): FormInterface
4955
*/
5056
$facetStats = $searchResult->getFacetStats();
5157

58+
$facets = $metadata->getFacetableAttributes();
59+
5260
/**
5361
* Here is an example of the facet distribution array
5462
*
@@ -70,8 +78,8 @@ public function build(SearchResult $searchResult): FormInterface
7078
* @var array<string, int> $values
7179
*/
7280
foreach ($searchResult->getFacetDistribution() as $name => $values) {
73-
if ($this->facetFormBuilder->supports($name, $values, $facetStats[$name] ?? null)) {
74-
$this->facetFormBuilder->build($facetsFormBuilder, $name, $values, $facetStats[$name] ?? null);
81+
if ($this->facetFormBuilder->supports($facets[$name], $values, $facetStats[$name] ?? null)) {
82+
$this->facetFormBuilder->build($facetsFormBuilder, $facets[$name], $values, $facetStats[$name] ?? null);
7583
}
7684
}
7785

src/Form/Type/RangeType.php

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Setono\SyliusMeilisearchPlugin\Form\Type;
6+
7+
use Symfony\Component\Form\AbstractType;
8+
use Symfony\Component\Form\Extension\Core\Type\NumberType;
9+
use Symfony\Component\Form\FormBuilderInterface;
10+
11+
final class RangeType extends AbstractType
12+
{
13+
public function buildForm(FormBuilderInterface $builder, array $options): void
14+
{
15+
$builder
16+
->add('min', NumberType::class, [
17+
'label' => 'Min',
18+
])
19+
->add('max', NumberType::class, [
20+
'label' => 'Max',
21+
])
22+
;
23+
}
24+
25+
public function getBlockPrefix(): string
26+
{
27+
return 'setono_sylius_meilisearch_range';
28+
}
29+
}

src/Meilisearch/Builder/FilterBuilder.php

-29
This file was deleted.

0 commit comments

Comments
 (0)