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

Add sorting of results #12

Merged
merged 6 commits into from
Aug 22, 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
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
"psalm/plugin-phpunit": "^0.18.4",
"setono/code-quality-pack": "^2.8.2",
"sylius/sylius": "~1.12.18",
"symfony/browser-kit": "^5.4 || ^6.4 || ^7.0",
"symfony/debug-bundle": "^5.4 || ^6.4 || ^7.0",
"symfony/dotenv": "^5.4 || ^6.4 || ^7.0",
"symfony/http-client": "^5.4 || ^6.4 || ^7.0",
Expand Down Expand Up @@ -107,7 +108,7 @@
"analyse": "psalm",
"check-style": "ecs check",
"fix-style": "ecs check --fix",
"phpunit": "phpunit",
"phpunit": "phpunit --exclude-group=functional",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's easier to use suites, don't you? I think we did the same in the order edit plugin

"rector": "rector"
}
}
32 changes: 5 additions & 27 deletions src/Controller/Action/SearchAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,10 @@
namespace Setono\SyliusMeilisearchPlugin\Controller\Action;

use Doctrine\Persistence\ManagerRegistry;
use Meilisearch\Client;
use Setono\Doctrine\ORMTrait;
use Setono\SyliusMeilisearchPlugin\Config\Index;
use Setono\SyliusMeilisearchPlugin\Document\Metadata\Facet;
use Setono\SyliusMeilisearchPlugin\Document\Metadata\MetadataFactoryInterface;
use Setono\SyliusMeilisearchPlugin\Engine\SearchEngineInterface;
use Setono\SyliusMeilisearchPlugin\Form\Builder\SearchFormBuilderInterface;
use Setono\SyliusMeilisearchPlugin\Meilisearch\Builder\FilterBuilderInterface;
use Setono\SyliusMeilisearchPlugin\Model\IndexableInterface;
use Setono\SyliusMeilisearchPlugin\Resolver\IndexName\IndexNameResolverInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Twig\Environment;
Expand All @@ -23,43 +18,26 @@
{
use ORMTrait;

public function __construct(

Check failure on line 21 in src/Controller/Action/SearchAction.php

View workflow job for this annotation

GitHub Actions / Backwards Compatibility Check

The parameter $indexNameResolver of Setono\SyliusMeilisearchPlugin\Controller\Action\SearchAction#__construct() changed from Setono\SyliusMeilisearchPlugin\Resolver\IndexName\IndexNameResolverInterface to a non-contravariant Setono\SyliusMeilisearchPlugin\Form\Builder\SearchFormBuilderInterface

Check failure on line 21 in src/Controller/Action/SearchAction.php

View workflow job for this annotation

GitHub Actions / Backwards Compatibility Check

The parameter $client of Setono\SyliusMeilisearchPlugin\Controller\Action\SearchAction#__construct() changed from Meilisearch\Client to a non-contravariant Setono\SyliusMeilisearchPlugin\Engine\SearchEngineInterface
ManagerRegistry $managerRegistry,
private readonly Environment $twig,
private readonly IndexNameResolverInterface $indexNameResolver,
private readonly Client $client,
private readonly MetadataFactoryInterface $metadataFactory,
private readonly SearchFormBuilderInterface $searchFormBuilder,
private readonly FilterBuilderInterface $filterBuilder,
private readonly Index $index,
private readonly int $hitsPerPage,
private readonly SearchEngineInterface $searchEngine,
) {
$this->managerRegistry = $managerRegistry;
}

public function __invoke(Request $request): Response
{
$q = $request->query->get('q');
Assert::nullOrString($q);
$query = $request->query->get('q');
Assert::nullOrString($query);

$page = (int) $request->query->get('p', 1);
$page = max(1, $page);

$metadata = $this->metadataFactory->getMetadataFor($this->index->document);

$searchResult = $this->client->index($this->indexNameResolver->resolve($this->index))->search($q, [
'facets' => array_map(static fn (Facet $facet) => $facet->name, $metadata->getFacets()),
'filter' => $this->filterBuilder->build($request),
'sort' => ['price:asc'],
'hitsPerPage' => $this->hitsPerPage,
'page' => $page,
]);
$searchResult = $this->searchEngine->execute($query, $request->query->all());

$searchForm = $this->searchFormBuilder->build($searchResult);
$searchForm->handleRequest($request);

$items = [];

/** @var array{entityClass: class-string<IndexableInterface>, entityId: mixed} $hit */
foreach ($searchResult->getHits() as $hit) {
$items[] = $this->getManager($hit['entityClass'])->find($hit['entityClass'], $hit['entityId']);
Expand Down
46 changes: 46 additions & 0 deletions src/Engine/SearchEngine.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php

declare(strict_types=1);

namespace Setono\SyliusMeilisearchPlugin\Engine;

use Meilisearch\Client;
use Meilisearch\Search\SearchResult;
use Setono\SyliusMeilisearchPlugin\Config\Index;
use Setono\SyliusMeilisearchPlugin\Document\Metadata\Facet;
use Setono\SyliusMeilisearchPlugin\Document\Metadata\MetadataFactoryInterface;
use Setono\SyliusMeilisearchPlugin\Meilisearch\Builder\FilterBuilderInterface;
use Setono\SyliusMeilisearchPlugin\Resolver\IndexName\IndexNameResolverInterface;

final class SearchEngine implements SearchEngineInterface
{
public function __construct(
private readonly MetadataFactoryInterface $metadataFactory,
private readonly FilterBuilderInterface $filterBuilder,
private readonly Index $index,
private readonly IndexNameResolverInterface $indexNameResolver,
private readonly Client $client,
private readonly int $hitsPerPage,
) {
}

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

$metadata = $this->metadataFactory->getMetadataFor($this->index->document);

$searchParams = [
'facets' => array_map(static fn (Facet $facet) => $facet->name, $metadata->getFacets()),
'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);
}
}
12 changes: 12 additions & 0 deletions src/Engine/SearchEngineInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

declare(strict_types=1);

namespace Setono\SyliusMeilisearchPlugin\Engine;

use Meilisearch\Search\SearchResult;

interface SearchEngineInterface
{
public function execute(?string $query, array $parameters = []): SearchResult;
}
6 changes: 4 additions & 2 deletions src/Form/Builder/SearchFormBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,10 @@ public function build(SearchResult $searchResult): FormInterface

$searchFormBuilder->add('sort', ChoiceType::class, [
'choices' => [
'Price: Low to High' => 'price:asc',
'Price: High to Low' => 'price:desc',
'Cheapest first' => 'price:asc',
'Biggest discount' => 'discount:desc',
'Newest first' => 'createdAt:desc',
'Relevance' => '',
],
'required' => false,
'placeholder' => 'Sort by',
Expand Down
6 changes: 2 additions & 4 deletions src/Meilisearch/Builder/FilterBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,14 @@

namespace Setono\SyliusMeilisearchPlugin\Meilisearch\Builder;

use Symfony\Component\HttpFoundation\Request;

// todo this should be refactored
final class FilterBuilder implements FilterBuilderInterface
{
public function build(Request $request): array
public function build(array $parameters): array

Check failure on line 10 in src/Meilisearch/Builder/FilterBuilder.php

View workflow job for this annotation

GitHub Actions / Backwards Compatibility Check

The parameter $request of Setono\SyliusMeilisearchPlugin\Meilisearch\Builder\FilterBuilder#build() changed from Symfony\Component\HttpFoundation\Request to a non-contravariant array
{
$filters = [];

$query = $request->query->has('facets') ? $request->query->all('facets') : $request->query->all();
$query = (array) ($parameters['facets'] ?? $parameters);

if (isset($query['onSale'])) {
$filters[] = 'onSale = true';
Expand Down
7 changes: 1 addition & 6 deletions src/Meilisearch/Builder/FilterBuilderInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,7 @@

namespace Setono\SyliusMeilisearchPlugin\Meilisearch\Builder;

use Symfony\Component\HttpFoundation\Request;

interface FilterBuilderInterface
{
/**
* Takes a Symfony request and returns a filter ready for the Meilisearch client
*/
public function build(Request $request): array;
public function build(array $parameters): array;

Check failure on line 9 in src/Meilisearch/Builder/FilterBuilderInterface.php

View workflow job for this annotation

GitHub Actions / Backwards Compatibility Check

The parameter $request of Setono\SyliusMeilisearchPlugin\Meilisearch\Builder\FilterBuilderInterface#build() changed from Symfony\Component\HttpFoundation\Request to a non-contravariant array

Check failure on line 9 in src/Meilisearch/Builder/FilterBuilderInterface.php

View workflow job for this annotation

GitHub Actions / Backwards Compatibility Check

The parameter $request of Setono\SyliusMeilisearchPlugin\Meilisearch\Builder\FilterBuilderInterface#build() changed from Symfony\Component\HttpFoundation\Request to array

Check failure on line 9 in src/Meilisearch/Builder/FilterBuilderInterface.php

View workflow job for this annotation

GitHub Actions / Backwards Compatibility Check

Parameter 0 of Setono\SyliusMeilisearchPlugin\Meilisearch\Builder\FilterBuilderInterface#build() changed name from request to parameters
}
12 changes: 9 additions & 3 deletions src/Resources/config/services/conditional/search.xml
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,19 @@
<service id="Setono\SyliusMeilisearchPlugin\Controller\Action\SearchAction" public="true">
<argument type="service" id="doctrine"/>
<argument type="service" id="twig"/>
<argument type="service" id="setono_sylius_meilisearch.resolver.index_name"/>
<argument type="service" id="Meilisearch\Client"/>
<argument type="service" id="Setono\SyliusMeilisearchPlugin\Document\Metadata\MetadataFactoryInterface"/>
<argument type="service" id="Setono\SyliusMeilisearchPlugin\Form\Builder\SearchFormBuilderInterface"/>
<argument type="service" id="Setono\SyliusMeilisearchPlugin\Engine\SearchEngineInterface" />
</service>

<service id="Setono\SyliusMeilisearchPlugin\Engine\SearchEngine">
<argument type="service" id="Setono\SyliusMeilisearchPlugin\Document\Metadata\MetadataFactoryInterface"/>
<argument type="service" id="Setono\SyliusMeilisearchPlugin\Meilisearch\Builder\FilterBuilderInterface"/>
<argument type="service" id="setono_sylius_meilisearch.index.search"/>
<argument type="service" id="setono_sylius_meilisearch.resolver.index_name"/>
<argument type="service" id="Meilisearch\Client"/>
<argument>%setono_sylius_meilisearch.search.hits_per_page%</argument>
</service>

<service id="Setono\SyliusMeilisearchPlugin\Engine\SearchEngineInterface" alias="Setono\SyliusMeilisearchPlugin\Engine\SearchEngine" />
</services>
</container>
76 changes: 76 additions & 0 deletions tests/Functional/SearchTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<?php

declare(strict_types=1);

namespace Setono\SyliusMeilisearchPlugin\Tests\Functional;

use Setono\SyliusMeilisearchPlugin\Engine\SearchEngine;
use Symfony\Bundle\FrameworkBundle\KernelBrowser;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;

/** @group functional */
final class SearchTest extends WebTestCase
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very nice to have tests! :D

{
private static KernelBrowser $client;

protected function setUp(): void
{
parent::setUp();

self::ensureKernelShutdown();
self::$client = static::createClient(['environment' => 'test', 'debug' => true]);
}

public function testItProvidesSearchResults(): void
{
/** @var SearchEngine $searchEngine */
$searchEngine = self::getContainer()->get(SearchEngine::class);
$result = $searchEngine->execute('jeans');

self::assertSame(8, $result->getHitsCount());
}

public function testItSortsSearchResultsByLowestPrice(): void
{
/** @var SearchEngine $searchEngine */
$searchEngine = self::getContainer()->get(SearchEngine::class);
$result = $searchEngine->execute('jeans', ['sort' => 'price:asc']);

self::assertSame(8, $result->getHitsCount());

$previousKey = null;
foreach ($result->getHits() as $key => $hit) {
if ($previousKey === null) {
$previousKey = $key;

continue;
}

$previousHit = (array) $result->getHit($previousKey);
self::assertGreaterThanOrEqual($previousHit['price'], $hit['price']);
$previousKey = $key;
}
}

public function testItSortsSearchResultsByNewestDate(): void
{
/** @var SearchEngine $searchEngine */
$searchEngine = self::getContainer()->get(SearchEngine::class);
$result = $searchEngine->execute('jeans', ['sort' => 'createdAt:desc']);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This small thing createdAt:desc made me think about whether we should snake case properties inside Meilisearch? If there would ever be a problem with camel case? I am thinking about web servers ignoring case, browsers ignoring case or whatever. I don't know. Do any of you know? It will give a lot of other problems though :)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO it shouldn't make any troubles.


self::assertSame(8, $result->getHitsCount());

$previousKey = null;
foreach ($result->getHits() as $key => $hit) {
if ($previousKey === null) {
$previousKey = $key;

continue;
}

$previousHit = (array) $result->getHit($previousKey);
self::assertLessThanOrEqual($previousHit['createdAt'], $hit['createdAt']);
$previousKey = $key;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

declare(strict_types=1);

namespace Setono\SyliusMeilisearchPlugin\Tests\DataMapper\Product;
namespace Setono\SyliusMeilisearchPlugin\Tests\Unit\DataMapper\Product;

use Doctrine\Common\Collections\ArrayCollection;
use PHPUnit\Framework\TestCase;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

declare(strict_types=1);

namespace Setono\SyliusMeilisearchPlugin\Tests\DependencyInjection;
namespace Setono\SyliusMeilisearchPlugin\Tests\Unit\DependencyInjection;

use Matthias\SymfonyDependencyInjectionTest\PhpUnit\AbstractExtensionTestCase;
use Setono\SyliusMeilisearchPlugin\Controller\Action\SearchAction;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

declare(strict_types=1);

namespace Setono\SyliusMeilisearchPlugin\Tests\Document\Metadata;
namespace Setono\SyliusMeilisearchPlugin\Tests\Unit\Document\Metadata;

use PHPUnit\Framework\TestCase;
use Setono\SyliusMeilisearchPlugin\Document\Attribute\Facet;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

declare(strict_types=1);

namespace Setono\SyliusMeilisearchPlugin\Tests\Resolver\IndexName;
namespace Setono\SyliusMeilisearchPlugin\Tests\Unit\Resolver\IndexName;

use PHPUnit\Framework\TestCase;
use Prophecy\PhpUnit\ProphecyTrait;
Expand Down
Loading