Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
No results found
Show changes
Commits on Source (7)
Showing
with 619 additions and 44 deletions
...@@ -17,7 +17,7 @@ ...@@ -17,7 +17,7 @@
], ],
"require": { "require": {
"php": ">=8.1", "php": ">=8.1",
"ramsey/collection": "^1.2", "ramsey/collection": "^2",
"elasticsearch/elasticsearch": "^8.5", "elasticsearch/elasticsearch": "^8.5",
"vlucas/phpdotenv": "^5.4.1" "vlucas/phpdotenv": "^5.4.1"
}, },
......
...@@ -20,7 +20,11 @@ return [ ...@@ -20,7 +20,11 @@ return [
], ],
'rating' => [ 'rating' => [
'type' => 'double', 'type' => 'double',
'index' => false, 'index' => true,
],
'new' => [
'type' => 'boolean',
'index' => true
], ],
'popular' => [ 'popular' => [
'type' => 'double', 'type' => 'double',
......
...@@ -23,27 +23,37 @@ class Aggregation ...@@ -23,27 +23,37 @@ class Aggregation
public function convertToQuery(): void public function convertToQuery(): void
{ {
$this->aggregations->add( $queryFilterCollection = $this->criteria->getFilters()->getFilterCollectionByType(FilterType::QUERY);
AggsFacetTerms::create(
'keyword_facet', if (false === $this->criteria->getSearch()->isEmpty() || false === $this->criteria->getFilters()->isEmpty()) {
'keyword_facet'
) $this->aggregations->add(
); AggsFacetTerms::create(
'keyword_facet', 'keyword_facet'
$this->aggregations->add( )
AggsFacetStats::create( );
'number_facet',
'number_facet' $this->aggregations->add(
) AggsFacetStats::create(
); 'number_facet', 'number_facet'
)
$postFilterCollection = $this->criteria->getFilters()->getFilterCollectionByType(FilterType::POST); );
$filterAggregation = new FilterAggregation($this->configuration, $postFilterCollection); $filterAggregation = new FilterAggregation($this->configuration, $queryFilterCollection);
$filterAggregation->updateRequestAggregation($this->aggregations); $filterAggregation->updateRequestAggregation($this->aggregations);
$fullAggregation = new FullAggregation($this->configuration, $postFilterCollection); $fullAggregation = new FullAggregation($this->configuration, $queryFilterCollection);
$fullAggregation->updateRequestAggregation($this->aggregations); $fullAggregation->updateRequestAggregation($this->aggregations);
}
if (false === $this->criteria->getAggs()->isEmpty()) {
$propertyAggregation = new PropertyAggregation(
$this->configuration,
$queryFilterCollection,
$this->criteria->getAggs(),
);
$propertyAggregation->updateRequestAggregation($this->aggregations);
}
} }
public function getAggregation(): AggsCollection public function getAggregation(): AggsCollection
......
<?php
declare(strict_types=1);
namespace IQDEV\ElasticSearch\Converter\Request\Aggregation;
use IQDEV\ElasticSearch\Configuration;
use IQDEV\ElasticSearch\Converter\Request\FilterQuery;
use IQDEV\ElasticSearch\Criteria\Filter\Collection\FilterCollection;
use IQDEV\ElasticSearch\Document\Property\PropertyType;
use IQDEV\ElasticSearch\Search\Aggs\Aggs;
use IQDEV\ElasticSearch\Search\Aggs\AggsCollection;
use IQDEV\ElasticSearch\Search\Aggs\Terms;
class PropertyAggregation
{
public function __construct(
private readonly Configuration $configuration,
private readonly FilterCollection $filterCollection,
private readonly \IQDEV\ElasticSearch\Criteria\Aggs\AggsCollection $aggsCollection,
) {
}
public function updateRequestAggregation(AggsCollection $original): void
{
$queryFilterBuilder = new FilterQuery($this->configuration, $this->filterCollection);
$query = $queryFilterBuilder->getQuery();
/** @var \IQDEV\ElasticSearch\Criteria\Aggs\Aggs $aggs */
foreach ($this->aggsCollection as $aggs) {
$property = $aggs->getProperty();
if ($property->getType() !== PropertyType::BASE) {
continue;
}
$aggs = new Aggs($property->getKey());
if (false === $query->isEmpty()) {
$aggs->setQuery($query);
$aggs->addAggs(
(new Aggs($property->getKey()))
->setTerms(new Terms($property->getKey()))
);
} else {
$aggs->setTerms(new Terms($property->getKey()));
}
$aggs->addAggs(
(new Aggs($property->getKey()))
->setTerms(new Terms($property->getKey()))
);
$original->add($aggs);
}
}
}
...@@ -40,9 +40,7 @@ class CriteriaRequestBuilder extends RequestBuilder ...@@ -40,9 +40,7 @@ class CriteriaRequestBuilder extends RequestBuilder
$this->setFilter(); $this->setFilter();
} }
if (false === $this->criteria->getSearch()->isEmpty() || false === $this->criteria->getFilters()->isEmpty()) { $this->setAggregations();
$this->setAggregations();
}
} }
public function setPagination(): void public function setPagination(): void
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
namespace IQDEV\ElasticSearch\Converter\Result; namespace IQDEV\ElasticSearch\Converter\Result;
use Elastic\Elasticsearch\Response\Elasticsearch; use Elastic\Elasticsearch\Response\Elasticsearch;
use IQDEV\ElasticSearch\Configuration;
use IQDEV\ElasticSearch\Document\Product; use IQDEV\ElasticSearch\Document\Product;
use IQDEV\ElasticSearch\Facet\FacetResult; use IQDEV\ElasticSearch\Facet\FacetResult;
use IQDEV\ElasticSearch\Facet\FacetType; use IQDEV\ElasticSearch\Facet\FacetType;
...@@ -13,7 +14,7 @@ use IQDEV\ElasticSearch\Result; ...@@ -13,7 +14,7 @@ use IQDEV\ElasticSearch\Result;
final class EsResponseToResult final class EsResponseToResult
{ {
public function fromResponse(Elasticsearch $response): Result public function fromResponse(Elasticsearch $response, Configuration $configuration): Result
{ {
$catalogSearchResult = new Result(); $catalogSearchResult = new Result();
...@@ -41,6 +42,8 @@ final class EsResponseToResult ...@@ -41,6 +42,8 @@ final class EsResponseToResult
$this->parseNumberFacet($data, $catalogSearchResult); $this->parseNumberFacet($data, $catalogSearchResult);
} }
$this->parsePropertyFacet($data, $catalogSearchResult, $configuration);
return $catalogSearchResult; return $catalogSearchResult;
} }
...@@ -93,10 +96,12 @@ final class EsResponseToResult ...@@ -93,10 +96,12 @@ final class EsResponseToResult
$facet = new FacetResult(FacetType::LIST, $code); $facet = new FacetResult(FacetType::LIST, $code);
foreach ($valueBucket as $value) { foreach ($valueBucket as $value) {
$count = 0; $count = $value['doc_count'];
if (isset($bucketsFiltered[$code][$value['key']])) { if (isset($bucketsFiltered[$code][$value['key']])) {
$count = $bucketsFiltered[$code][$value['key']]['doc_count']; $count = $bucketsFiltered[$code][$value['key']]['doc_count'];
} elseif (isset($bucketsFiltered[$code])) {
$count = 0;
} }
$facet->products->add( $facet->products->add(
...@@ -150,4 +155,30 @@ final class EsResponseToResult ...@@ -150,4 +155,30 @@ final class EsResponseToResult
$catalogSearchResult->getFacets()->add($facet); $catalogSearchResult->getFacets()->add($facet);
} }
} }
private function parsePropertyFacet(array $data, Result $catalogSearchResult, Configuration $configuration)
{
$properties = array_keys($configuration->getMapping()['properties']);
foreach ($data['aggregations'] as $key => $aggs) {
if (!in_array($key, $properties, true)) {
continue;
}
$facet = new FacetResult(FacetType::LIST, $key);
$buckets = array_key_exists('buckets', $aggs) ? $aggs['buckets'] : $aggs[$key]['buckets'];
foreach ($buckets as $bucket) {
$code = $bucket['key'];
$count = $bucket['doc_count'];
$facet->products->add(
FacetItemList::create(
$code,
$count
)
);
}
$catalogSearchResult->getFacets()->add($facet);
}
}
} }
<?php
declare(strict_types=1);
namespace IQDEV\ElasticSearch\Criteria\Aggs;
use IQDEV\ElasticSearch\Document\Property\Property;
class Aggs
{
public function __construct(
private readonly Property $property,
) {
}
public function getProperty(): Property
{
return $this->property;
}
}
<?php
namespace IQDEV\ElasticSearch\Criteria\Aggs;
use Ramsey\Collection\AbstractCollection;
class AggsCollection extends AbstractCollection
{
/**
* @inheritDoc
*/
public function getType(): string
{
return Aggs::class;
}
}
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
namespace IQDEV\ElasticSearch\Criteria; namespace IQDEV\ElasticSearch\Criteria;
use IQDEV\ElasticSearch\Criteria\Aggs\AggsCollection;
use IQDEV\ElasticSearch\Criteria\Filter\Collection\FilterCollection; use IQDEV\ElasticSearch\Criteria\Filter\Collection\FilterCollection;
use IQDEV\ElasticSearch\Criteria\Order\OrderCollection; use IQDEV\ElasticSearch\Criteria\Order\OrderCollection;
use IQDEV\ElasticSearch\Criteria\Search\SearchCollection; use IQDEV\ElasticSearch\Criteria\Search\SearchCollection;
...@@ -13,12 +14,15 @@ final class Criteria ...@@ -13,12 +14,15 @@ final class Criteria
private OrderCollection $sorting; private OrderCollection $sorting;
private Pagination $pagination; private Pagination $pagination;
private AggsCollection $aggregations;
public function __construct() public function __construct()
{ {
$this->search = new SearchCollection(); $this->search = new SearchCollection();
$this->filters = new FilterCollection(); $this->filters = new FilterCollection();
$this->sorting = new OrderCollection(); $this->sorting = new OrderCollection();
$this->pagination = new Pagination(); $this->pagination = new Pagination();
$this->aggregations = new AggsCollection();
} }
public function getSearch(): SearchCollection public function getSearch(): SearchCollection
...@@ -41,11 +45,17 @@ final class Criteria ...@@ -41,11 +45,17 @@ final class Criteria
return $this->pagination; return $this->pagination;
} }
public function getAggs(): AggsCollection
{
return $this->aggregations;
}
public function __clone(): void public function __clone(): void
{ {
$this->search = clone $this->search; $this->search = clone $this->search;
$this->filters = clone $this->filters; $this->filters = clone $this->filters;
$this->sorting = clone $this->sorting; $this->sorting = clone $this->sorting;
$this->pagination = clone $this->pagination; $this->pagination = clone $this->pagination;
$this->aggregations = clone $this->aggregations;
} }
} }
...@@ -7,17 +7,17 @@ use IQDEV\ElasticSearch\Criteria\Filter\FilterValue; ...@@ -7,17 +7,17 @@ use IQDEV\ElasticSearch\Criteria\Filter\FilterValue;
class FilterKeyword implements FilterValue class FilterKeyword implements FilterValue
{ {
/** /**
* @param string|string[] $value * @param string|string[]|bool $value
*/ */
public function __construct( public function __construct(
public string|array $value public string|array|bool $value
) { ) {
} }
/** /**
* @return string|string[] * @return string|string[]|bool
*/ */
public function value(): string|array public function value(): string|array|bool
{ {
return $this->value; return $this->value;
} }
......
...@@ -10,7 +10,7 @@ class QueryMatch implements Esable ...@@ -10,7 +10,7 @@ class QueryMatch implements Esable
{ {
public function __construct( public function __construct(
private readonly string $key, private readonly string $key,
private readonly string $value, private readonly mixed $value,
) { ) {
} }
......
...@@ -10,7 +10,7 @@ class Search ...@@ -10,7 +10,7 @@ class Search
{ {
public function __construct( public function __construct(
private readonly Property $property, private readonly Property $property,
private readonly string $value, private readonly mixed $value,
) { ) {
} }
...@@ -19,7 +19,7 @@ class Search ...@@ -19,7 +19,7 @@ class Search
return $this->property; return $this->property;
} }
public function getValue(): string public function getValue(): mixed
{ {
return $this->value; return $this->value;
} }
......
...@@ -113,7 +113,7 @@ class ProductDocument implements Document ...@@ -113,7 +113,7 @@ class ProductDocument implements Document
$result = array_replace_recursive($document, $this->properties); $result = array_replace_recursive($document, $this->properties);
if (true === $this->skipEmpty) { if (true === $this->skipEmpty) {
$result = ArrayHelper::array_filter_recursive($result); $result = ArrayHelper::array_filter_recursive($result, static fn ($val) => $val !== null || $val === false);
} }
return $result; return $result;
......
...@@ -11,9 +11,13 @@ final class KeywordFacet extends Facet ...@@ -11,9 +11,13 @@ final class KeywordFacet extends Facet
*/ */
public function es(): array public function es(): array
{ {
$value = is_array($this->value) ?
array_map(static fn($value) => (string) $value, $this->value) :
(string) $this->value;
return [ return [
'facet_code' => $this->property->getKey(), 'facet_code' => $this->property->getKey(),
'facet_value' => (string) $this->value, 'facet_value' => $value
]; ];
} }
} }
...@@ -8,11 +8,11 @@ final class Terms implements Esable ...@@ -8,11 +8,11 @@ final class Terms implements Esable
{ {
/** /**
* @param string $key * @param string $key
* @param string|float|array<string|float> $value * @param string|float|bool|array<string|float> $value
*/ */
public function __construct( public function __construct(
private string $key, private string $key,
private string|float|array $value private mixed $value
) { ) {
} }
......
...@@ -35,6 +35,6 @@ class SearchService implements Searchable ...@@ -35,6 +35,6 @@ class SearchService implements Searchable
'body' => $request->es(), 'body' => $request->es(),
]); ]);
return $this->esResponseToResult->fromResponse($response); return $this->esResponseToResult->fromResponse($response, $this->configuration);
} }
} }
<?php
namespace IQDEV\ElasticSearchTests\Filter;
use IQDEV\ElasticSearch\Criteria\Aggs\Aggs;
use IQDEV\ElasticSearch\Criteria\Criteria;
use IQDEV\ElasticSearch\Criteria\Filter\Collection\FilterGroupCollection;
use IQDEV\ElasticSearch\Criteria\Filter\Field;
use IQDEV\ElasticSearch\Criteria\Filter\Filter;
use IQDEV\ElasticSearch\Criteria\Filter\FilterOperator;
use IQDEV\ElasticSearch\Criteria\Filter\Value\FilterNumber;
use IQDEV\ElasticSearch\Criteria\Query\SearchQuery;
use IQDEV\ElasticSearch\Document\Property\Property;
use IQDEV\ElasticSearchTests\AbstractTestCase;
use IQDEV\ElasticSearchTests\Helpers\FormatData;
use IQDEV\ElasticSearchTests\Service\SearchClient;
/**
* Тестирование агрегирующих функций для прямых свойств документов
*/
class AggsPropsTest extends AbstractTestCase
{
public function testCategoryAggs()
{
$criteria = new Criteria();
$criteria->getAggs()->add(
new Aggs(
new Property('new')
)
);
$q = new SearchQuery($criteria);
$handler = SearchClient::getInstance();
$result = $handler->handle($q)->result;
$expected = [
'hits' => [
'h1',
'h2',
'h3',
'p1',
's1',
's2',
's3',
's4',
],
'facets' => [
0 => [
'code' => 'new',
'label' => null,
'type' => 'list',
'items' => [
'list' => [
0 => [
'label' => null,
'value' => 0,
'count' => 3,
'active' => true
],
1 => [
'label' => null,
'value' => 1,
'count' => 5,
'active' => true
]
],
'range' => []
]
],
]
];
$this->assertEqualsCanonicalizing($expected, FormatData::formatDataWFacets($result));
}
public function testCategoryAggsWFilters()
{
$criteria = new Criteria();
$criteria->getAggs()->add(
new Aggs(
new Property('new')
)
);
$criteria->getAggs()->add(
new Aggs(
new Property('rating')
)
);
$filterCollectionPrice = new FilterGroupCollection([
new Filter(
new Field('price'),
FilterOperator::GTE,
new FilterNumber(105)
)
]);
$criteria->getFilters()->add($filterCollectionPrice);
$q = new SearchQuery($criteria);
$handler = SearchClient::getInstance();
$result = $handler->handle($q)->result;
$expected = [
'hits' => [
'h2',
'h3',
'p1'
],
'facets' => [
0 => [
'code' => 'brand',
'label' => null,
'type' => 'list',
'items' => [
'list' => [
0 => [
'label' => null,
'value' => 'adidas',
'count' => 0,
'active' => false
],
1 => [
'label' => null,
'value' => 'nike',
'count' => 1,
'active' => true
],
3 => [
'label' => null,
'value' => 'rebook',
'count' => 2,
'active' => true
]
],
'range' => []
]
],
1 => [
'code' => 'color',
'label' => null,
'type' => 'list',
'items' => [
'list' => [
0 => [
'label' => null,
'value' => 'blue',
'count' => 0,
'active' => false
],
1 => [
'label' => null,
'value' => 'green',
'count' => 0,
'active' => false
],
2 => [
'label' => null,
'value' => 'red',
'count' => 0,
'active' => false
],
3 => [
'label' => null,
'value' => 'white',
'count' => 3,
'active' => true
]
],
'range' => []
]
],
2 => [
'code' => 'size',
'label' => null,
'type' => 'list',
'items' => [
'list' => [
0 => [
"label" => null,
"value" => "43",
"count" => 0,
"active" => false
],
1 => [
"label" => null,
"value" => "46",
"count" => 0,
"active" => false
],
2 => [
"label" => null,
"value" => "47",
"count" => 0,
"active" => false
],
3 => [
"label" => null,
"value" => "xl",
"count" => 2,
"active" => true
],
4 => [
"label" => null,
"value" => "xxl",
"count" => 1,
"active" => true
],
],
'range' => []
]
],
3 => [
'code' => 'price',
'label' => null,
'type' => 'range',
'items' => [
'list' => [],
'range' => [
0 => [
'label' => null,
'count' => 3,
'active' => true,
'fullRange' => [
'min' => 100.0,
'max' => 107.0
],
'activeRange' => [
'min' => 105.0,
'max' => 107.0
]
]
]
],
],
4 => [
'code' => 'new',
'label' => null,
'type' => 'list',
'items' => [
'list' => [
0 => [
'label' => null,
'value' => "0",
'count' => 1,
'active' => true
],
1 => [
'label' => null,
'value' => "1",
'count' => 2,
'active' => true
]
],
'range' => []
]
],
5 => [
'code' => 'rating',
'label' => null,
'type' => 'list',
'items' => [
'list' => [
0 => [
'label' => null,
'value' => "3",
'count' => 3,
'active' => true
],
],
'range' => []
]
],
]
];
$this->assertEqualsCanonicalizing($expected, FormatData::formatDataWFacets($result));
}
}
...@@ -69,7 +69,7 @@ class IndexesTest extends AbstractTestCase ...@@ -69,7 +69,7 @@ class IndexesTest extends AbstractTestCase
] ]
]); ]);
$esResponseToResult = new EsResponseToResult(); $esResponseToResult = new EsResponseToResult();
$result = $esResponseToResult->fromResponse($response); $result = $esResponseToResult->fromResponse($response, $this->configuration);
unset($updateData['type']); unset($updateData['type']);
$expected = [ $expected = [
...@@ -80,4 +80,4 @@ class IndexesTest extends AbstractTestCase ...@@ -80,4 +80,4 @@ class IndexesTest extends AbstractTestCase
$this->assertEqualsCanonicalizing($expected, FormatData::formatDataProducts($result)); $this->assertEqualsCanonicalizing($expected, FormatData::formatDataProducts($result));
} }
} }
\ No newline at end of file
...@@ -490,4 +490,147 @@ class QueryTest extends AbstractTestCase ...@@ -490,4 +490,147 @@ class QueryTest extends AbstractTestCase
$this->assertEqualsCanonicalizing($expected, FormatData::formatDataWFacets($result)); $this->assertEqualsCanonicalizing($expected, FormatData::formatDataWFacets($result));
} }
}
\ No newline at end of file public function testGlobalFilterMinPrice()
{
$filter = [
'price' => [
'key' => 'price',
'lower' => 103,
]
];
$criteria = new Criteria();
$filterCollectionQueryPrice = new FilterGroupCollection([
new Filter(
new Field($filter['price']['key']),
FilterOperator::GTE,
new FilterNumber($filter['price']['lower'])
),
]);
$filterCollectionQueryPrice->setFilterType(FilterType::QUERY);
$criteria->getFilters()->add($filterCollectionQueryPrice);
$q = new SearchQuery($criteria);
$handler = SearchClient::getInstance();
$result = $handler->handle($q)->result;
$expected = [
'hits' => [
's4',
'h1',
'h2',
'h3',
'p1',
],
"facets" => [
0 => [
"code" => "brand",
"label" => null,
"type" => "list",
"items" => [
"list" => [
0 => [
"label" => null,
"value" => "nike",
"count" => 3,
"active" => true,
],
1 => [
"label" => null,
"value" => "rebook",
"count" => 2,
"active" => true,
]
],
"range" => [],
],
],
1 => [
"code" => "color",
"label" => null,
"type" => "list",
"items" => [
"list" => [
0 => [
"label" => null,
"value" => "green",
"count" => 1,
"active" => true,
],
1 => [
"label" => null,
"value" => "red",
"count" => 1,
"active" => true,
],
2 => [
"label" => null,
"value" => "white",
"count" => 3,
"active" => true,
],
],
"range" => [],
],
],
2 => [
"code" => "size",
"label" => null,
"type" => "list",
"items" => [
"list" => [
0 => [
"label" => null,
"count" => 1,
"value" => "43",
"active" => true,
],
1 => [
"label" => null,
"value" => "xl",
"count" => 3,
"active" => true,
],
2 => [
"label" => null,
"value" => "xxl",
"count" => 1,
"active" => true,
],
],
"range" => [],
],
],
3 => [
"code" => "price",
"label" => null,
"type" => "range",
"items" => [
"list" => [],
"range" => [
0 => [
"label" => null,
"count" => 5,
"active" => true,
"fullRange" => [
"min" => 103.0,
"max" => 107.0,
],
"activeRange" => [
"min" => 103.0,
"max" => 107.0,
]
]
]
]
]
]
];
$this->assertEqualsCanonicalizing($expected, FormatData::formatDataWFacets($result));
}
}
...@@ -61,6 +61,9 @@ class TestIndexProvider implements IndexProvider ...@@ -61,6 +61,9 @@ class TestIndexProvider implements IndexProvider
} }
} }
$document->setByConfiguration($this->configuration, 'new', $product['new']);
$document->setByConfiguration($this->configuration, 'rating', $product['rating']);
$product['type'] = $product['type'] ?? null; $product['type'] = $product['type'] ?? null;
switch ($product['type']) { switch ($product['type']) {
case 'update': case 'update':
...@@ -119,4 +122,4 @@ class TestIndexProvider implements IndexProvider ...@@ -119,4 +122,4 @@ class TestIndexProvider implements IndexProvider
{ {
return null; return null;
} }
} }
\ No newline at end of file