Skip to content

Commit

Permalink
OP-225: Common text search logic
Browse files Browse the repository at this point in the history
  • Loading branch information
GracjanJozefczyk committed Jun 3, 2024
1 parent 9bdb055 commit 30544d4
Show file tree
Hide file tree
Showing 7 changed files with 78 additions and 126 deletions.
18 changes: 10 additions & 8 deletions features/shop/searching_products_by_a_partial_name.feature
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,17 @@ Feature: Filtering products

Background:
Given the store operates on a channel named "Web-US" in "USD" currency
And the store classifies its products as "Cars"
And there is a product named "Volksvagen Polo" in the store
And there is a product named "Volvo XC90" in the store
And there is a product named "Polonez Caro" in the store
And there is a product named "Porsche Carrera GT" in the store
And these products belongs primarily to "Cars" taxon
And the store classifies its products as "Shirts"
And there is a product named "Loose white designer T-Shirt" in the store
And there is a product named "Everyday white basic T-Shirt" in the store
And there is a product named "Ribbed copper slim fit Tee" in the store
And there is a product named "Oversize white cotton T-Shirt" in the store
And there is a product named "Raglan grey & black Tee" in the store
And there is a product named "Sport basic white T-Shirt" in the store
And these products belongs primarily to "Shirts" taxon
And the data is populated to Elasticsearch

@api
Scenario: Filtering products by name
When I search the products by "vol" phrase
Then I should see 2 products
When I search the products by "shirt" phrase
Then I should see 4 products
27 changes: 19 additions & 8 deletions spec/QueryBuilder/ContainsNameQueryBuilderSpec.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,23 +11,27 @@
namespace spec\BitBag\SyliusElasticsearchPlugin\QueryBuilder;

use BitBag\SyliusElasticsearchPlugin\PropertyNameResolver\ConcatedNameResolverInterface;
use BitBag\SyliusElasticsearchPlugin\PropertyNameResolver\SearchPropertyNameResolverRegistryInterface;
use BitBag\SyliusElasticsearchPlugin\QueryBuilder\ContainsNameQueryBuilder;
use BitBag\SyliusElasticsearchPlugin\QueryBuilder\QueryBuilderInterface;
use Elastica\Query\MatchQuery;
use Elastica\Query\MultiMatch;
use PhpSpec\ObjectBehavior;
use Sylius\Component\Locale\Context\LocaleContextInterface;

final class ContainsNameQueryBuilderSpec extends ObjectBehavior
{
function let(
LocaleContextInterface $localeContext,
SearchPropertyNameResolverRegistryInterface $searchPropertyNameResolverRegistry,
ConcatedNameResolverInterface $productNameNameResolver
): void {
$this->beConstructedWith(
$localeContext,
$productNameNameResolver,
'name_property'
$searchPropertyNameResolverRegistry,
'AUTO'
);

$searchPropertyNameResolverRegistry->getPropertyNameResolvers()->willReturn([$productNameNameResolver]);
}

function it_is_initializable(): void
Expand All @@ -42,23 +46,30 @@ function it_implements_query_builder_interface(): void

function it_builds_query(
LocaleContextInterface $localeContext,
SearchPropertyNameResolverRegistryInterface $searchPropertyNameResolverRegistry,
ConcatedNameResolverInterface $productNameNameResolver
): void {
$localeContext->getLocaleCode()->willReturn('en');

$productNameNameResolver->resolvePropertyName('en')->willReturn('en');
$productNameNameResolver->resolvePropertyName('en')->willReturn('name_en');

$searchPropertyNameResolverRegistry->getPropertyNameResolvers()->willReturn([$productNameNameResolver]);

$this->buildQuery(['name_property' => 'Book'])->shouldBeAnInstanceOf(MatchQuery::class);
$query = $this->buildQuery(['name' => 'Book']);
$query->shouldBeAnInstanceOf(MultiMatch::class);
}

function it_builds_returned_null_if_property_is_null(
function it_returns_null_when_no_query(
LocaleContextInterface $localeContext,
SearchPropertyNameResolverRegistryInterface $searchPropertyNameResolverRegistry,
ConcatedNameResolverInterface $productNameNameResolver
): void {
$localeContext->getLocaleCode()->willReturn('en');

$productNameNameResolver->resolvePropertyName('en')->willReturn('en');
$productNameNameResolver->resolvePropertyName('en')->willReturn('name_en');

$searchPropertyNameResolverRegistry->getPropertyNameResolvers()->willReturn([$productNameNameResolver]);

$this->buildQuery(['name_property' => null])->shouldBeEqualTo(null);
$this->buildQuery([])->shouldReturn(null);
}
}
70 changes: 16 additions & 54 deletions spec/QueryBuilder/SiteWideProductsQueryBuilderSpec.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,46 +4,44 @@

namespace spec\BitBag\SyliusElasticsearchPlugin\QueryBuilder;

use BitBag\SyliusElasticsearchPlugin\PropertyNameResolver\ConcatedNameResolverInterface;
use BitBag\SyliusElasticsearchPlugin\PropertyNameResolver\SearchPropertyNameResolverRegistryInterface;
use BitBag\SyliusElasticsearchPlugin\QueryBuilder\QueryBuilderInterface;
use BitBag\SyliusElasticsearchPlugin\QueryBuilder\SiteWideProductsQueryBuilder;
use Elastica\Query\AbstractQuery;
use Elastica\Query\BoolQuery;
use Elastica\Query\MultiMatch;
use Elastica\Query\Term;
use Elastica\Query\Terms;
use PhpSpec\ObjectBehavior;
use Sylius\Component\Locale\Context\LocaleContextInterface;

final class SiteWideProductsQueryBuilderSpec extends ObjectBehavior
{
private $isEnabeldQuery;
private AbstractQuery $isEnabeldQuery;

private $hasChannelQuery;
private AbstractQuery $hasChannelQuery;

private $fuzziness;
private AbstractQuery $containsNameQuery;

function let(
SearchPropertyNameResolverRegistryInterface $searchPropertyNameResolverRegistry,
LocaleContextInterface $localeContext,
QueryBuilderInterface $isEnabledQueryBuilder,
QueryBuilderInterface $hasChannelQueryBuilder
QueryBuilderInterface $hasChannelQueryBuilder,
QueryBuilderInterface $containsNameQueryBuilder,
): void {
$localeContext->getLocaleCode()->willReturn('en_US');
$searchPropertyNameResolverRegistry->getPropertyNameResolvers()->willReturn([]);
$this->isEnabeldQuery = new Term();
$this->isEnabeldQuery->setTerm('enabled', true);
$isEnabledQueryBuilder->buildQuery([])->willReturn($this->isEnabeldQuery);

$this->hasChannelQuery = new Terms('channels');
$this->hasChannelQuery->setTerms(['web_us']);
$hasChannelQueryBuilder->buildQuery([])->willReturn($this->hasChannelQuery);
$this->fuzziness = 'AUTO';

$this->containsNameQuery = new MultiMatch();
$this->containsNameQuery->setQuery('bmw');
$containsNameQueryBuilder->buildQuery(['query' => 'bmw'])->willReturn($this->containsNameQuery);

$this->beConstructedWith(
$searchPropertyNameResolverRegistry,
$localeContext,
$isEnabledQueryBuilder,
$hasChannelQueryBuilder,
$this->fuzziness
$containsNameQueryBuilder
);
}

Expand All @@ -57,48 +55,12 @@ function it_implements_query_builder_interface(): void
$this->shouldHaveType(QueryBuilderInterface::class);
}

function it_throws_an_exception_if_query_is_not_present_in_data(): void
{
$this->shouldThrow(\RuntimeException::class)->during('buildQuery', [['not_relevant_key' => 'value']]);
}

function it_throws_an_exception_if_query_is_not_a_string(): void
{
$this->shouldThrow(\RuntimeException::class)->during('buildQuery', [['query' => new \stdClass()]]);
}

function it_builds_multi_match_query_with_provided_query_string(): void
{
$expectedMultiMatch = new MultiMatch();
$expectedMultiMatch->setQuery('bmw');
$expectedMultiMatch->setFuzziness($this->fuzziness);
$expectedMultiMatch->setFields([]);
$expectedQuery = new BoolQuery();
$expectedQuery->addMust($expectedMultiMatch);
$expectedQuery->addFilter($this->isEnabeldQuery);
$expectedQuery->addFilter($this->hasChannelQuery);

$this->buildQuery(['query' => 'bmw'])->shouldBeLike($expectedQuery);
}

function it_builds_multi_match_query_with_provided_query_string_and_fields_from_registry(
SearchPropertyNameResolverRegistryInterface $searchPropertyNameResolverRegistry,
ConcatedNameResolverInterface $firstPropertyNameResolver,
ConcatedNameResolverInterface $secondPropertyNameResolver
): void {
$firstPropertyNameResolver->resolvePropertyName('en_US')->shouldBeCalled()->willReturn('property_1_en_us');
$secondPropertyNameResolver->resolvePropertyName('en_US')->shouldBeCalled()->willReturn('property_2_en_us');
$searchPropertyNameResolverRegistry->getPropertyNameResolvers()->willReturn(
[$firstPropertyNameResolver, $secondPropertyNameResolver]
);
$expectedMultiMatch = new MultiMatch();
$expectedMultiMatch->setQuery('bmw');
$expectedMultiMatch->setFuzziness($this->fuzziness);
$expectedMultiMatch->setFields(['property_1_en_us', 'property_2_en_us']);
$expectedQuery = new BoolQuery();
$expectedQuery->addMust($expectedMultiMatch);
$expectedQuery->addFilter($this->isEnabeldQuery);
$expectedQuery->addFilter($this->hasChannelQuery);
$expectedQuery->addMust($this->isEnabeldQuery);
$expectedQuery->addMust($this->hasChannelQuery);
$expectedQuery->addMust($this->containsNameQuery);

$this->buildQuery(['query' => 'bmw'])->shouldBeLike($expectedQuery);
}
Expand Down
26 changes: 15 additions & 11 deletions src/QueryBuilder/ContainsNameQueryBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,35 +12,39 @@

namespace BitBag\SyliusElasticsearchPlugin\QueryBuilder;

use BitBag\SyliusElasticsearchPlugin\PropertyNameResolver\ConcatedNameResolverInterface;
use BitBag\SyliusElasticsearchPlugin\PropertyNameResolver\SearchPropertyNameResolverRegistryInterface;
use Elastica\Query\AbstractQuery;
use Elastica\Query\MatchQuery;
use Elastica\Query\MultiMatch;
use Sylius\Component\Locale\Context\LocaleContextInterface;

final class ContainsNameQueryBuilder implements QueryBuilderInterface
{
public function __construct(
private LocaleContextInterface $localeContext,
private ConcatedNameResolverInterface $productNameNameResolver,
private string $namePropertyPrefix
private SearchPropertyNameResolverRegistryInterface $searchProperyNameResolverRegistry,
private string $fuzziness = 'AUTO'
) {
}

public function buildQuery(array $data): ?AbstractQuery
{
$localeCode = $this->localeContext->getLocaleCode();
$propertyName = $this->productNameNameResolver->resolvePropertyName($localeCode);
$query = $data['name'] ?? $data['query'] ?? null;

if (!$name = $data[$this->namePropertyPrefix]) {
if (null === $query || '' === $query) {
return null;
}

$nameQuery = new MatchQuery();
$fields = [];
foreach ($this->searchProperyNameResolverRegistry->getPropertyNameResolvers() as $propertyNameResolver) {
$fields[] = $propertyNameResolver->resolvePropertyName($localeCode);
}

$nameQuery->setFieldQuery($propertyName, $name);
$nameQuery->setFieldFuzziness($propertyName, 2);
$nameQuery->setFieldMinimumShouldMatch($propertyName, 2);
$multiMatch = new MultiMatch();
$multiMatch->setQuery($query);
$multiMatch->setFuzziness($this->fuzziness);
$multiMatch->setFields($fields);

return $nameQuery;
return $multiMatch;
}
}
55 changes: 15 additions & 40 deletions src/QueryBuilder/SiteWideProductsQueryBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,60 +12,35 @@

namespace BitBag\SyliusElasticsearchPlugin\QueryBuilder;

use BitBag\SyliusElasticsearchPlugin\PropertyNameResolver\SearchPropertyNameResolverRegistryInterface;
use Elastica\Query\AbstractQuery;
use Elastica\Query\BoolQuery;
use Elastica\Query\MultiMatch;
use Sylius\Component\Locale\Context\LocaleContextInterface;

final class SiteWideProductsQueryBuilder implements QueryBuilderInterface
{
public const QUERY_KEY = 'query';

public function __construct(
private SearchPropertyNameResolverRegistryInterface $searchProperyNameResolverRegistry,
private LocaleContextInterface $localeContext,
private QueryBuilderInterface $isEnabledQueryBuilder,
private QueryBuilderInterface $hasChannelQueryBuilder,
private string $fuzziness
private QueryBuilderInterface $containsNameQueryBuilder
) {
}

public function buildQuery(array $data): ?AbstractQuery
{
if (!array_key_exists(self::QUERY_KEY, $data)) {
throw new \RuntimeException(
sprintf(
'Could not build search products query because there\'s no "query" key in provided data. ' .
'Got the following keys: %s',
implode(', ', array_keys($data))
)
);
}
$query = $data[self::QUERY_KEY];
if (!is_string($query)) {
throw new \RuntimeException(
sprintf(
'Could not build search products query because the provided "query" is expected to be a string ' .
'but "%s" is given.',
is_object($query) ? get_class($query) : gettype($query)
)
);
}
$boolQuery = new BoolQuery();

$multiMatch = new MultiMatch();
$multiMatch->setQuery($query);
$multiMatch->setFuzziness($this->fuzziness);
$fields = [];
foreach ($this->searchProperyNameResolverRegistry->getPropertyNameResolvers() as $propertyNameResolver) {
$fields[] = $propertyNameResolver->resolvePropertyName($this->localeContext->getLocaleCode());
}
$multiMatch->setFields($fields);
$bool = new BoolQuery();
$bool->addMust($multiMatch);
$bool->addFilter($this->isEnabledQueryBuilder->buildQuery([]));
$bool->addFilter($this->hasChannelQueryBuilder->buildQuery([]));
$boolQuery->addMust($this->isEnabledQueryBuilder->buildQuery([]));
$boolQuery->addMust($this->hasChannelQueryBuilder->buildQuery([]));

$nameQuery = $this->containsNameQueryBuilder->buildQuery($data);
$this->addMustIfNotNull($nameQuery, $boolQuery);

return $bool;
return $boolQuery;
}

private function addMustIfNotNull(?AbstractQuery $query, BoolQuery $boolQuery): void
{
if (null !== $query) {
$boolQuery->addMust($query);
}
}
}
2 changes: 1 addition & 1 deletion src/Resources/config/services/finder.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<container xmlns="http://symfony.com/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<service id="bitbag_sylius_elasticsearch_plugin.finder.named_products" class="BitBag\SyliusElasticsearchPlugin\Finder\NamedProductsFinder">
<argument type="service" id="bitbag_sylius_elasticsearch_plugin.query_builder.site_wide_products" />
<argument type="service" id="bitbag_sylius_elasticsearch_plugin.query_builder.contains_name" />
<argument type="service" id="fos_elastica.finder.bitbag_shop_product" />
</service>

Expand Down
6 changes: 2 additions & 4 deletions src/Resources/config/services/query_builder.xml
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@

<service id="bitbag_sylius_elasticsearch_plugin.query_builder.contains_name" class="BitBag\SyliusElasticsearchPlugin\QueryBuilder\ContainsNameQueryBuilder">
<argument type="service" id="sylius.context.locale" />
<argument type="service" id="bitbag_sylius_elasticsearch_plugin.property_name_resolver.name" />
<argument>%bitbag_es_shop_name_property_prefix%</argument>
<argument type="service" id="bitbag_sylius_elasticsearch_plugin.search_property_name_resolver_registry" />
</service>

<service id="bitbag_sylius_elasticsearch_plugin.query_builder.has_product_taxon" class="BitBag\SyliusElasticsearchPlugin\QueryBuilder\HasTaxonQueryBuilder">
Expand Down Expand Up @@ -82,10 +81,9 @@
</service>

<service id="bitbag_sylius_elasticsearch_plugin.query_builder.site_wide_products" class="BitBag\SyliusElasticsearchPlugin\QueryBuilder\SiteWideProductsQueryBuilder">
<argument type="service" id="bitbag_sylius_elasticsearch_plugin.search_property_name_resolver_registry" />
<argument type="service" id="sylius.context.locale" />
<argument type="service" id="bitbag_sylius_elasticsearch_plugin.query_builder.is_enabled" />
<argument type="service" id="bitbag_sylius_elasticsearch_plugin.query_builder.has_channel" />
<argument type="service" id="bitbag_sylius_elasticsearch_plugin.query_builder.contains_name" />
<argument>%bitbag_es_fuzziness%</argument>
</service>

Expand Down

0 comments on commit 30544d4

Please sign in to comment.