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 @@
],
"require": {
"php": ">=8.1",
"ramsey/collection": "^1.2",
"ramsey/collection": "^2",
"elasticsearch/elasticsearch": "^8.5",
"vlucas/phpdotenv": "^5.4.1"
},
......
......@@ -20,7 +20,11 @@ return [
],
'rating' => [
'type' => 'double',
'index' => false,
'index' => true,
],
'new' => [
'type' => 'boolean',
'index' => true
],
'popular' => [
'type' => 'double',
......
......@@ -23,27 +23,37 @@ class Aggregation
public function convertToQuery(): void
{
$this->aggregations->add(
AggsFacetTerms::create(
'keyword_facet',
'keyword_facet'
)
);
$this->aggregations->add(
AggsFacetStats::create(
'number_facet',
'number_facet'
)
);
$postFilterCollection = $this->criteria->getFilters()->getFilterCollectionByType(FilterType::POST);
$filterAggregation = new FilterAggregation($this->configuration, $postFilterCollection);
$filterAggregation->updateRequestAggregation($this->aggregations);
$fullAggregation = new FullAggregation($this->configuration, $postFilterCollection);
$fullAggregation->updateRequestAggregation($this->aggregations);
$queryFilterCollection = $this->criteria->getFilters()->getFilterCollectionByType(FilterType::QUERY);
if (false === $this->criteria->getSearch()->isEmpty() || false === $this->criteria->getFilters()->isEmpty()) {
$this->aggregations->add(
AggsFacetTerms::create(
'keyword_facet', 'keyword_facet'
)
);
$this->aggregations->add(
AggsFacetStats::create(
'number_facet', 'number_facet'
)
);
$filterAggregation = new FilterAggregation($this->configuration, $queryFilterCollection);
$filterAggregation->updateRequestAggregation($this->aggregations);
$fullAggregation = new FullAggregation($this->configuration, $queryFilterCollection);
$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
......
<?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
$this->setFilter();
}
if (false === $this->criteria->getSearch()->isEmpty() || false === $this->criteria->getFilters()->isEmpty()) {
$this->setAggregations();
}
$this->setAggregations();
}
public function setPagination(): void
......
......@@ -3,6 +3,7 @@
namespace IQDEV\ElasticSearch\Converter\Result;
use Elastic\Elasticsearch\Response\Elasticsearch;
use IQDEV\ElasticSearch\Configuration;
use IQDEV\ElasticSearch\Document\Product;
use IQDEV\ElasticSearch\Facet\FacetResult;
use IQDEV\ElasticSearch\Facet\FacetType;
......@@ -13,7 +14,7 @@ use IQDEV\ElasticSearch\Result;
final class EsResponseToResult
{
public function fromResponse(Elasticsearch $response): Result
public function fromResponse(Elasticsearch $response, Configuration $configuration): Result
{
$catalogSearchResult = new Result();
......@@ -41,6 +42,8 @@ final class EsResponseToResult
$this->parseNumberFacet($data, $catalogSearchResult);
}
$this->parsePropertyFacet($data, $catalogSearchResult, $configuration);
return $catalogSearchResult;
}
......@@ -93,10 +96,12 @@ final class EsResponseToResult
$facet = new FacetResult(FacetType::LIST, $code);
foreach ($valueBucket as $value) {
$count = 0;
$count = $value['doc_count'];
if (isset($bucketsFiltered[$code][$value['key']])) {
$count = $bucketsFiltered[$code][$value['key']]['doc_count'];
} elseif (isset($bucketsFiltered[$code])) {
$count = 0;
}
$facet->products->add(
......@@ -150,4 +155,30 @@ final class EsResponseToResult
$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 @@
namespace IQDEV\ElasticSearch\Criteria;
use IQDEV\ElasticSearch\Criteria\Aggs\AggsCollection;
use IQDEV\ElasticSearch\Criteria\Filter\Collection\FilterCollection;
use IQDEV\ElasticSearch\Criteria\Order\OrderCollection;
use IQDEV\ElasticSearch\Criteria\Search\SearchCollection;
......@@ -13,12 +14,15 @@ final class Criteria
private OrderCollection $sorting;
private Pagination $pagination;
private AggsCollection $aggregations;
public function __construct()
{
$this->search = new SearchCollection();
$this->filters = new FilterCollection();
$this->sorting = new OrderCollection();
$this->pagination = new Pagination();
$this->aggregations = new AggsCollection();
}
public function getSearch(): SearchCollection
......@@ -41,11 +45,17 @@ final class Criteria
return $this->pagination;
}
public function getAggs(): AggsCollection
{
return $this->aggregations;
}
public function __clone(): void
{
$this->search = clone $this->search;
$this->filters = clone $this->filters;
$this->sorting = clone $this->sorting;
$this->pagination = clone $this->pagination;
$this->aggregations = clone $this->aggregations;
}
}
......@@ -7,17 +7,17 @@ use IQDEV\ElasticSearch\Criteria\Filter\FilterValue;
class FilterKeyword implements FilterValue
{
/**
* @param string|string[] $value
* @param string|string[]|bool $value
*/
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;
}
......
......@@ -10,7 +10,7 @@ class QueryMatch implements Esable
{
public function __construct(
private readonly string $key,
private readonly string $value,
private readonly mixed $value,
) {
}
......
......@@ -10,7 +10,7 @@ class Search
{
public function __construct(
private readonly Property $property,
private readonly string $value,
private readonly mixed $value,
) {
}
......@@ -19,7 +19,7 @@ class Search
return $this->property;
}
public function getValue(): string
public function getValue(): mixed
{
return $this->value;
}
......
......@@ -113,7 +113,7 @@ class ProductDocument implements Document
$result = array_replace_recursive($document, $this->properties);
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;
......
......@@ -11,9 +11,13 @@ final class KeywordFacet extends Facet
*/
public function es(): array
{
$value = is_array($this->value) ?
array_map(static fn($value) => (string) $value, $this->value) :
(string) $this->value;
return [
'facet_code' => $this->property->getKey(),
'facet_value' => (string) $this->value,
'facet_value' => $value
];
}
}
......@@ -8,11 +8,11 @@ final class Terms implements Esable
{
/**
* @param string $key
* @param string|float|array<string|float> $value
* @param string|float|bool|array<string|float> $value
*/
public function __construct(
private string $key,
private string|float|array $value
private mixed $value
) {
}
......
......@@ -35,6 +35,6 @@ class SearchService implements Searchable
'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
]
]);
$esResponseToResult = new EsResponseToResult();
$result = $esResponseToResult->fromResponse($response);
$result = $esResponseToResult->fromResponse($response, $this->configuration);
unset($updateData['type']);
$expected = [
......@@ -80,4 +80,4 @@ class IndexesTest extends AbstractTestCase
$this->assertEqualsCanonicalizing($expected, FormatData::formatDataProducts($result));
}
}
\ No newline at end of file
}
......@@ -490,4 +490,147 @@ class QueryTest extends AbstractTestCase
$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
}
}
$document->setByConfiguration($this->configuration, 'new', $product['new']);
$document->setByConfiguration($this->configuration, 'rating', $product['rating']);
$product['type'] = $product['type'] ?? null;
switch ($product['type']) {
case 'update':
......@@ -119,4 +122,4 @@ class TestIndexProvider implements IndexProvider
{
return null;
}
}
\ No newline at end of file
}