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 (27)
Showing
with 656 additions and 565 deletions
IQ_ES_HOSTS=127.0.0.1:9200
IQ_ES_USER=
IQ_ES_PASSWORD=
IQ_ES_PRODUCT_SEARCH_INDEX=
\ No newline at end of file
.DS_Store
/composer.lock
/vendor/
/.idea/
\ No newline at end of file
/.idea/
.env
.phpunit.result.cache
\ No newline at end of file
......@@ -9,7 +9,6 @@
"email": "p.piligrimov@iqdev.digital"
}
],
"version": "0.0.1",
"type": "library",
"keywords": [
"search",
......@@ -17,9 +16,8 @@
"php"
],
"require": {
"php": ">=7.4",
"ramsey/collection": "^1.2",
"iqdev/search-dc": "dev-main",
"php": ">=8.1",
"ramsey/collection": "^2",
"elasticsearch/elasticsearch": "^8.5",
"vlucas/phpdotenv": "^5.4.1"
},
......@@ -28,10 +26,21 @@
"IQDEV\\ElasticSearch\\": "src/ElasticSearch/"
}
},
"repositories": [
{
"type": "vcs",
"url": "ssh://git@gitlab.iqdev.digital:8422/piligrimov/search-dc.git"
"autoload-dev": {
"psr-4": {
"IQDEV\\ElasticSearchTests\\": "tests/"
}
},
"require-dev": {
"phpunit/phpunit": "^9.5",
"symfony/var-dumper": "^6.3"
},
"scripts": {
"tests": "php ./vendor/bin/phpunit --testdox --verbose"
},
"config": {
"allow-plugins": {
"php-http/discovery": true
}
]
}
}
<?xml version="1.0" encoding="UTF-8"?>
<phpunit colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnFailure="false"
bootstrap="tests/bootstrap.php">
<testsuites>
<testsuite name="Project Tests Suite">
<directory>tests</directory>
</testsuite>
</testsuites>
<coverage processUncoveredFiles="true">
<include>
<directory suffix=".php">src/ElasticSearch/</directory>
</include>
</coverage>
</phpunit>
\ No newline at end of file
# Проведение тестов
Для работы корректности фильтрации и индексации данных в эластике необходимо подключение к elasticsearch
## Индексация elasticsearch
Эластик поднимается и настраивается отдельно.
Настроить подключение к эластику в переменных окружения .env
```dotenv
IQ_ES_HOSTS=http://127.0.0.1:9200
IQ_ES_USER=elastic
IQ_ES_PASSWORD=passsword
IQ_ES_PRODUCT_SEARCH_INDEX=product-test
```
Для наполнения эластика данными выполнить команду:
`php tests/CLI/DefaultSeed.php`
## Запуск тестов
Для проведения тестов можно использовать команду
`php composer tests`
\ No newline at end of file
......@@ -20,7 +20,11 @@ return [
],
'rating' => [
'type' => 'double',
'index' => false,
'index' => true,
],
'new' => [
'type' => 'boolean',
'index' => true
],
'popular' => [
'type' => 'double',
......
<?php
namespace IQDEV\ElasticSearch\Converter;
use IQDEV\ElasticSearch\Order\OrderAscType;
use IQDEV\ElasticSearch\Order\OrderDescType;
use IQDEV\ElasticSearch\Order\OrderField;
use IQDEV\ElasticSearch\Order\OrderKeywordProperty;
use IQDEV\ElasticSearch\Order\OrderNumberProperty;
use IQDEV\ElasticSearch\Search\Aggs\Aggs;
use IQDEV\ElasticSearch\Search\Aggs\AggsFacetStats;
use IQDEV\ElasticSearch\Search\Aggs\AggsFacetTerms;
use IQDEV\ElasticSearch\Search\BoolQuery\FilterKeywordFacet;
use IQDEV\ElasticSearch\Search\BoolQuery\FilterNumberFacet;
use IQDEV\ElasticSearch\Search\BoolQuery\Query;
use IQDEV\ElasticSearch\Search\BoolQuery\Terms;
use IQDEV\ElasticSearch\Search\Nested;
use IQDEV\ElasticSearch\Search\Pagination;
use IQDEV\ElasticSearch\Search\Request;
use IQDEV\Search\Criteria;
use IQDEV\Search\Document\Property\AttrType;
use IQDEV\Search\Document\Property\PropertyType;
use IQDEV\Search\FIlter\Filter;
use IQDEV\Search\Filter\FilterOperator;
use IQDEV\Search\Order\Order;
use IQDEV\Search\Order\OrderAscType as SOrderAscType;
use IQDEV\Search\Order\OrderDescType as SOrderDescType;
final class CriteriaToEsRequest
{
public function fromCriteria(Criteria $criteria): Request
{
$request = new Request();
$request = $this->pagination($request, $criteria);
$request = $this->order($request, $criteria);
$request = $this->filter($request, $criteria);
$request = $this->aggs($request, $criteria);
return $request;
}
private function pagination(Request $request, Criteria $criteria): Request
{
$request = clone $request;
$request->setPagination(new Pagination($criteria->pagination()->limit, $criteria->pagination()->offset));
return $request;
}
private function order(Request $request, Criteria $criteria): Request
{
$request = clone $request;
if (true === $criteria->sorting()->isEmpty()) {
return $request;
}
foreach ($criteria->sorting() as $order) {
/** @var Order $order */
$direction = null;
if ($order->orderType() instanceof SOrderAscType) {
$direction = new OrderAscType();
}
if ($order->orderType() instanceof SOrderDescType) {
$direction = new OrderDescType();
}
if ($order->orderBy() instanceof AttrType) {
$request->getSort()->add(new OrderField($order->orderBy()->key(), $direction));
} elseif ($order->orderBy() instanceof PropertyType) {
if ($order->orderBy()->type() === PropertyType::TYPE_KEYWORD) {
$request->getSort()->add(new OrderKeywordProperty($order->orderBy()->key(), $direction));
} elseif ($order->orderBy()->type() === PropertyType::TYPE_NUMBER) {
$request->getSort()->add(new OrderNumberProperty($order->orderBy()->key(), $direction));
}
}
}
return $request;
}
private function filter(Request $request, Criteria $criteria): Request
{
$request = clone $request;
if ($criteria->filters()->isEmpty()) {
return $request;
}
foreach ($criteria->filters() as $filter) {
/** @var Filter $filter */
$value = $filter->value()->value();
$field = $filter->field()->value();
if ('search' === $field) {
if ($filter->operator()->value() === FilterOperator::CONTAINS) {
$request->addMatch(
'suggest_search_content',
[
'query' => $value,
],
);
} else {
$request->addMatch(
'full_search_content',
[
'query' => $value,
],
);
}
continue;
}
if ('category_id' === $field) {
$request->getQuery()->must(
new Terms('category_id', $filter->value()->value())
);
continue;
}
}
$keywordFilter = $this->getKeywordFilter($criteria);
if (false === $keywordFilter->isEmpty()) {
$keywordNestedFilter = new Nested();
$keywordNestedFilter->setPath('search_data')
->setQuery($keywordFilter);
$request->getPostFilter()->filter($keywordNestedFilter);
}
$numberFilter = $this->getNumberFilter($criteria);
if (false === $numberFilter->isEmpty()) {
$numberNestedFilter = new Nested();
$numberNestedFilter->setPath('search_data')
->setQuery($numberFilter);
$request->getPostFilter()->filter($numberNestedFilter);
}
return $request;
}
private function getNumberFilter(Criteria $criteria, array $excludeFilter = []): Query
{
$numberFilter = new Query();
$ranges = [];
foreach ($criteria->filters() as $filter) {
/** @var Filter $filter */
$value = $filter->value()->value();
$field = $filter->field()->value();
if (in_array($field, $excludeFilter, true)) {
continue;
}
if (in_array($filter->operator()->value(), [FilterOperator::LT, FilterOperator::LTE], true)) {
$ranges[$field][$filter->operator()->value()] = $value;
continue;
}
if (in_array($filter->operator()->value(), [FilterOperator::GT, FilterOperator::GTE], true)) {
$ranges[$field][$filter->operator()->value()] = $value;
}
}
if (false === empty($ranges)) {
foreach ($ranges as $field => $range) {
$numberFilter->filter(
new FilterNumberFacet(
$field,
$range
)
);
}
}
return $numberFilter;
}
private function getKeywordFilter(Criteria $criteria, array $excludeFilter = []): Query
{
$keywordFilter = new Query();
foreach ($criteria->filters() as $filter) {
/** @var Filter $filter */
$value = $filter->value()->value();
$field = $filter->field()->value();
if (in_array($field, $excludeFilter, true)) {
continue;
}
if (in_array($filter->operator()->value(), [FilterOperator::LT, FilterOperator::LTE], true)) {
continue;
}
if (in_array($filter->operator()->value(), [FilterOperator::GT, FilterOperator::GTE], true)) {
continue;
}
if ('search' === $field) {
continue;
}
if ('category_id' === $field) {
continue;
}
if (is_array($value)) {
$value = array_map(static fn($v) => (string)$v, $value);
} else {
$value = (string)$value;
}
$keywordFilter->filter(new FilterKeywordFacet($field, $value));
}
return $keywordFilter;
}
private function aggs(Request $request, Criteria $criteria): Request
{
$request = clone $request;
if ($criteria->filters()->isEmpty()) {
return $request;
}
$request->getAggs()->add(
AggsFacetTerms::create(
'keyword_facet',
'keyword_facet'
)
);
$request->getAggs()->add(
AggsFacetStats::create(
'number_facet',
'number_facet'
)
);
$getKey = static fn(string $type, string $name): string => sprintf('%s_facet_%s', $type, $name);
foreach ($criteria->filters() as $filter) {
/** @var Filter $filter */
$field = $filter->field()->value();
if (in_array(
$filter->operator()->value(),
[
FilterOperator::LT,
FilterOperator::LTE,
FilterOperator::GT,
FilterOperator::GTE,
],
true
)
) {
$aggsFiltered = new Aggs($getKey('number', $field));
$aggsFiltered->addAggs(
AggsFacetStats::create(
'agg_special',
'number_facet'
)
);
$queryNumberFiltered = new Query();
$keywordFilter = $this->getKeywordFilter($criteria);
$numberFilter = $this->getNumberFilter($criteria, [$field]);
if (false === $keywordFilter->isEmpty()) {
$nestedFilterKeyword = new Nested();
$nestedFilterKeyword->setPath('search_data')
->setQuery($keywordFilter);
$queryNumberFiltered->filter($nestedFilterKeyword);
}
if (false === $numberFilter->isEmpty()) {
$nestedFilterNumber = new Nested();
$nestedFilterNumber->setPath('search_data')
->setQuery($numberFilter);
$queryNumberFiltered->filter($nestedFilterNumber);
}
if ($queryNumberFiltered->isEmpty() === false) {
$aggsFiltered->setQuery($queryNumberFiltered);
} else {
$aggsFiltered->setNested((new Nested())->setPath('search_data'));
}
$request->getAggs()->add($aggsFiltered);
continue;
}
if (in_array($filter->operator()->value(), [], true)) {
continue;
}
if ('search' === $field) {
continue;
}
if ('category_id' === $field) {
continue;
}
$aggsFiltered = new Aggs($getKey('keyword', $field));
$aggsFiltered->addAggs(
AggsFacetTerms::create(
'agg_special',
'keyword_facet'
)
);
$queryKeywordFiltered = new Query();
$keywordFilter = $this->getKeywordFilter($criteria, [$field]);
$numberFilter = $this->getNumberFilter($criteria);
if (false === $keywordFilter->isEmpty()) {
$nestedFilterKeyword = new Nested();
$nestedFilterKeyword->setPath('search_data')
->setQuery($keywordFilter);
$queryKeywordFiltered->filter($nestedFilterKeyword);
}
if (false === $numberFilter->isEmpty()) {
$nestedFilterNumber = new Nested();
$nestedFilterNumber->setPath('search_data')
->setQuery($numberFilter);
$queryKeywordFiltered->filter($nestedFilterNumber);
}
if ($queryKeywordFiltered->isEmpty() === false) {
$aggsFiltered->setQuery($queryKeywordFiltered);
} else {
$aggsFiltered->setNested((new Nested())->setPath('search_data'));
}
$request->getAggs()->add($aggsFiltered);
}
$keywordFilter = $this->getKeywordFilter($criteria);
$numberFilter = $this->getNumberFilter($criteria);
$aggsKeywordFiltered = new Aggs('keyword_facet_filtered');
$aggsKeywordFiltered->addAggs(
AggsFacetTerms::create(
'all_keyword_facet_filtered',
'keyword_facet'
)
);
$queryKeywordFiltered = new Query();
$aggsNumberFiltered = new Aggs('number_facet_filtered');
$aggsNumberFiltered->addAggs(
AggsFacetStats::create(
'all_number_facet_filtered',
'number_facet'
)
);
$queryNumberFiltered = new Query();
if (false === $keywordFilter->isEmpty()) {
$nestedFilterKeyword = new Nested();
$nestedFilterKeyword->setPath('search_data')
->setQuery($keywordFilter);
$queryKeywordFiltered->filter($nestedFilterKeyword);
$queryNumberFiltered->filter($nestedFilterKeyword);
}
if (false === $numberFilter->isEmpty()) {
$nestedFilterNumber = new Nested();
$nestedFilterNumber->setPath('search_data')
->setQuery($numberFilter);
$queryKeywordFiltered->filter($nestedFilterNumber);
$queryNumberFiltered->filter($nestedFilterNumber);
}
if (false === $queryKeywordFiltered->isEmpty()) {
$aggsKeywordFiltered->setQuery($queryKeywordFiltered);
} else {
$aggsKeywordFiltered->setNested((new Nested())->setPath('search_data'));
}
if (false === $queryNumberFiltered->isEmpty()) {
$aggsNumberFiltered->setQuery($queryNumberFiltered);
} else {
$aggsNumberFiltered->setNested((new Nested())->setPath('search_data'));
}
$request->getAggs()
->add($aggsKeywordFiltered)
->add($aggsNumberFiltered);
return $request;
}
}
<?php
namespace IQDEV\ElasticSearch\Converter;
use Elastic\Elasticsearch\Response\Elasticsearch;
use IQDEV\Search\Document\Product;
use IQDEV\Search\Facet\Facet;
use IQDEV\Search\Facet\Item\FacetItemList;
use IQDEV\Search\Facet\Item\FacetItemRange;
use IQDEV\Search\Facet\Item\FacetItemRangeDTO;
use IQDEV\Search\Facet\Type\FacetListType;
use IQDEV\Search\Facet\Type\FacetRangeType;
use IQDEV\Search\Result;
final class EsResponseToResult
{
public function fromResponse(Elasticsearch $response): Result
{
$catalogSearchResult = new Result();
$data = $response->asArray();
if (isset($data['hits']['hits'])) {
foreach ($data['hits']['hits'] as $hit) {
if (isset($hit['_source'])) {
try {
$product = $this->productFromArray($hit['_source']);
$catalogSearchResult->getProducts()->add($product);
} catch (\Throwable $ex) {
continue;
}
}
}
$catalogSearchResult->setTotal((int)$data['hits']['total']['value']);
}
if (isset($data['aggregations']['keyword_facet']['agg_keyword_facet_code']['buckets'])) {
$buckets = $data['aggregations']['keyword_facet']['agg_keyword_facet_code']['buckets'];
$bucketsFiltered = [];
if (isset($data['aggregations']['keyword_facet_filtered']['all_keyword_facet_filtered']['agg_keyword_facet_code']['buckets'])) {
foreach ($data['aggregations']['keyword_facet_filtered']['all_keyword_facet_filtered']['agg_keyword_facet_code']['buckets'] as $bucket) {
$bucketsFiltered[$bucket['key']] = [];
foreach ($bucket['agg_keyword_facet_value']['buckets'] as $values) {
$bucketsFiltered[$bucket['key']][$values['key']] = $values;
}
}
}
foreach ($buckets as $bucket) {
$code = $bucket['key'];
if (isset($data['aggregations']["keyword_facet_$code"]['agg_special']['agg_keyword_facet_code']['buckets'])) {
$bucketsFiltered[$code] = [];
foreach ($data['aggregations']["keyword_facet_$code"]['agg_special']['agg_keyword_facet_code']['buckets'] as $filteredBucket) {
if ($filteredBucket['key'] === $code) {
foreach ($filteredBucket['agg_keyword_facet_value']['buckets'] as $values) {
$bucketsFiltered[$code][$values['key']] = $values;
}
}
}
}
}
$bucketsFiltered = array_filter($bucketsFiltered);
foreach ($buckets as $bucket) {
$code = $bucket['key'];
$valueBucket = $bucket['agg_keyword_facet_value']['buckets'];
$facet = new Facet(new FacetListType(), $code);
foreach ($valueBucket as $value) {
$count = 0;
if (isset($bucketsFiltered[$code][$value['key']])) {
$count = $bucketsFiltered[$code][$value['key']]['doc_count'];
}
$facet->products->add(
FacetItemList::create(
$value['key'],
$count,
isset($bucketsFiltered[$code][$value['key']])
)
);
}
$catalogSearchResult->getFacets()->add($facet);
}
}
if (isset($data['aggregations']['number_facet']['agg_number_facet_code']['buckets'])) {
$buckets = $data['aggregations']['number_facet']['agg_number_facet_code']['buckets'];
$bucketsFiltered = [];
if (isset($data['aggregations']['number_facet_filtered']['all_number_facet_filtered']['agg_number_facet_code']['buckets'])) {
foreach ($data['aggregations']['number_facet_filtered']['all_number_facet_filtered']['agg_number_facet_code']['buckets'] as $bucket) {
$bucketsFiltered[$bucket['key']] = $bucket['agg_number_facet_value'];
}
}
foreach ($buckets as $bucket) {
$code = $bucket['key'];
if (isset($data['aggregations']["number_facet_$code"]['agg_special']['agg_number_facet_code']['buckets'])) {
$bucketsFiltered[$code] = [];
foreach ($data['aggregations']["number_facet_$code"]['agg_special']['agg_number_facet_code']['buckets'] as $filteredBucked) {
if ($filteredBucked['key'] === $code) {
$bucketsFiltered[$code] = $filteredBucked['agg_number_facet_value'];
}
}
}
}
$bucketsFiltered = array_filter($bucketsFiltered);
foreach ($buckets as $bucket) {
$code = $bucket['key'];
$workBucket = $bucket['agg_number_facet_value'];
$selectedBuket = $bucketsFiltered[$code] ?? null;
$facet = new Facet(new FacetRangeType(), $code);
$facetItem = FacetItemRange::create(
FacetItemRangeDTO::create(
$workBucket['min'],
$workBucket['max'],
$workBucket['avg'],
$workBucket['sum']
),
isset($selectedBuket) ? FacetItemRangeDTO::create(
$selectedBuket['min'],
$selectedBuket['max'],
$selectedBuket['avg'],
$selectedBuket['sum']
) : FacetItemRangeDTO::create(),
$selectedBuket['count'] ?? $workBucket['count'],
isset($selectedBuket)
);
$facet->products->add($facetItem);
$catalogSearchResult->getFacets()->add($facet);
}
}
return $catalogSearchResult;
}
private function productFromArray(array $data): Product
{
if (!isset($data['data']['id'])) {
throw new \RuntimeException('id is not set');
}
$id = $data['data']['id'];
$title = $data['title'] ?? '';
$info = $data['data'] ?? [];
return new Product($id, $title, $info);
}
}
<?php
declare(strict_types=1);
namespace IQDEV\ElasticSearch\Converter\Request\Aggregation;
use IQDEV\ElasticSearch\Configuration;
use IQDEV\ElasticSearch\Criteria\Criteria;
use IQDEV\ElasticSearch\Criteria\Filter\FilterType;
use IQDEV\ElasticSearch\Search\Aggs\AggsCollection;
use IQDEV\ElasticSearch\Search\Aggs\AggsFacetStats;
use IQDEV\ElasticSearch\Search\Aggs\AggsFacetTerms;
class Aggregation
{
public function __construct(
private readonly Configuration $configuration,
private readonly Criteria $criteria,
private readonly AggsCollection $aggregations = new AggsCollection(),
) {
$this->convertToQuery();
}
public function convertToQuery(): void
{
$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
{
return $this->aggregations;
}
}
<?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\Criteria\Filter\Collection\FilterGroupCollection;
use IQDEV\ElasticSearch\Criteria\Filter\Filter;
use IQDEV\ElasticSearch\Search\Aggs\Aggs;
use IQDEV\ElasticSearch\Search\Aggs\AggsCollection;
use IQDEV\ElasticSearch\Search\Aggs\AggsFacetTerms;
use IQDEV\ElasticSearch\Search\Nested;
class FilterAggregation
{
private ?Aggs $aggregation = null;
public function __construct(
private readonly Configuration $configuration,
private readonly FilterCollection $filterCollection,
) {
}
public function updateRequestAggregation(AggsCollection $original): void
{
foreach ($this->filterCollection as $filterGroup) {
/** @var FilterGroupCollection $filterGroup */
foreach ($filterGroup as $filter) {
/** @var Filter $filter */
$field = $filter->field()->value();
$aggregation = new Aggs($this->getKey('keyword', $field));
$aggregation->addAggs(
AggsFacetTerms::create(
'agg_special',
'keyword_facet'
)
);
$queryFilterBuilder = new FilterQuery($this->configuration, $this->filterCollection, [$field]);
$query = $queryFilterBuilder->getQuery();
if (false === $query->isEmpty()) {
$aggregation->setQuery($query);
} else {
$aggregation->setNested((new Nested())->setPath('search_data'));
}
$original->add($aggregation);
}
}
}
private function getKey(string $type, string $name): string
{
return sprintf('%s_facet_%s', $type, $name);
}
}
<?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\Search\Aggs\Aggs;
use IQDEV\ElasticSearch\Search\Aggs\AggsCollection;
use IQDEV\ElasticSearch\Search\Aggs\AggsFacetStats;
use IQDEV\ElasticSearch\Search\Aggs\AggsFacetTerms;
use IQDEV\ElasticSearch\Search\Nested;
class FullAggregation
{
public function __construct(
private readonly Configuration $configuration,
private readonly FilterCollection $filterCollection,
) {
}
public function updateRequestAggregation(AggsCollection $original): void
{
$keywordQuery = (new FilterQuery($this->configuration, $this->filterCollection->getKeywordFilters()))->getQuery();
$numberQuery = (new FilterQuery($this->configuration, $this->filterCollection->getNumberFilters()))->getQuery();
$aggsKeywordFiltered = new Aggs('keyword_facet_filtered');
$aggsKeywordFiltered->addAggs(
AggsFacetTerms::create(
'all_keyword_facet_filtered',
'keyword_facet'
)
);
$aggsNumberFiltered = new Aggs('number_facet_filtered');
$aggsNumberFiltered->addAggs(
AggsFacetStats::create(
'all_number_facet_filtered',
'number_facet'
)
);
if (false === $keywordQuery->getFilter()->isEmpty()) {
foreach ($keywordQuery->getFilter() as $item) {
$numberQuery->getFilter()->add($item);
}
}
if (false === $numberQuery->getFilter()->isEmpty()) {
foreach ($numberQuery->getFilter() as $item) {
$keywordQuery->getFilter()->add($item);
}
}
if (false === $keywordQuery->getFilter()->isEmpty()) {
$aggsKeywordFiltered->setQuery($keywordQuery);
} else {
$aggsKeywordFiltered->setNested((new Nested())->setPath('search_data'));
}
if (false === $numberQuery->getFilter()->isEmpty()) {
$aggsNumberFiltered->setQuery($numberQuery);
} else {
$aggsNumberFiltered->setNested((new Nested())->setPath('search_data'));
}
$original->add($aggsKeywordFiltered)->add($aggsNumberFiltered);
}
}
<?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);
}
}
}
<?php
declare(strict_types=1);
namespace IQDEV\ElasticSearch\Converter\Request\Collection;
use IQDEV\ElasticSearch\Criteria\Filter\Collection\FilterCollection;
class NestedFilterCollection extends FilterCollection
{
}
<?php
declare(strict_types=1);
namespace IQDEV\ElasticSearch\Converter\Request\Collection;
use IQDEV\ElasticSearch\Criteria\Filter\Collection\FilterCollection;
class PropertyFilterCollection extends FilterCollection
{
}
<?php
declare(strict_types=1);
namespace IQDEV\ElasticSearch\Converter\Request;
use IQDEV\ElasticSearch\Configuration;
use IQDEV\ElasticSearch\Converter\Request\Aggregation\Aggregation;
use IQDEV\ElasticSearch\Criteria\Criteria;
use IQDEV\ElasticSearch\Criteria\Filter\Collection\PostFilterCollection;
use IQDEV\ElasticSearch\Criteria\Filter\Collection\QueryFilterCollection;
use IQDEV\ElasticSearch\Criteria\Search\Search;
use IQDEV\ElasticSearch\Criteria\Search\SearchQuery;
use IQDEV\ElasticSearch\Document\Property\PropertyType;
use IQDEV\ElasticSearch\Search\Pagination;
class CriteriaRequestBuilder extends RequestBuilder
{
public function __construct(
private readonly Configuration $configuration,
private readonly Criteria $criteria
) {
parent::__construct();
}
public function build(): void
{
$this->setPagination();
if (false === $this->criteria->getSorting()->isEmpty()) {
$this->setSort();
}
if (false === $this->criteria->getSearch()->isEmpty()) {
$this->setSearch();
}
if (false === $this->criteria->getFilters()->isEmpty()) {
$this->setFilter();
}
$this->setAggregations();
}
public function setPagination(): void
{
$request = clone $this->request;
$request->setPagination(new Pagination(
$this->criteria->getPagination()->limit,
$this->criteria->getPagination()->offset
));
$this->request = $request;
}
public function setSort(): void
{
$request = clone $this->request;
foreach ($this->criteria->getSorting() as $sort) {
$request->getSort()->add($sort);
}
$this->request = $request;
}
public function setSearch(): void
{
$request = clone $this->request;
foreach ($this->criteria->getSearch() as $search) {
/** @var Search $search */
$searchQuery = new SearchQuery($search);
if ($search->getProperty()->getType() === PropertyType::TEXT) {
$request->getQueryMatch()->add($searchQuery->toQueryMatch());
} else {
$request->getQuery()->getMust()->add($searchQuery->toMust($this->configuration));
}
}
$this->request = $request;
}
public function setFilter(): void
{
$queryFilterCollection = $this->criteria->getFilters()->getQueryFilterCollection();
if (false === $queryFilterCollection->isEmpty()) {
$this->setQueryFilter($queryFilterCollection);
}
$postFilterCollection = $this->criteria->getFilters()->getPostFilterCollection();
if (false === $postFilterCollection->isEmpty()) {
$this->setPostFilter($postFilterCollection);
}
}
private function setQueryFilter(QueryFilterCollection $filterCollection): void
{
$request = clone $this->request;
$filterQuery = new FilterQuery($this->configuration, $filterCollection);
$request->getQuery()->modify($filterQuery->getQuery());
$this->request = $request;
}
private function setPostFilter(PostFilterCollection $filterCollection): void
{
$request = clone $this->request;
$filterQuery = new FilterQuery($this->configuration, $filterCollection);
$request->getPostFilter()->modify($filterQuery->getQuery());
$this->request = $request;
}
public function setAggregations(): void
{
$request = clone $this->request;
$aggregation = new Aggregation($this->configuration, $this->criteria);
$request->setAggs($aggregation->getAggregation());
$this->request = $request;
}
}
<?php
declare(strict_types=1);
namespace IQDEV\ElasticSearch\Converter\Request;
use IQDEV\ElasticSearch\Configuration;
use IQDEV\ElasticSearch\Criteria\Criteria;
use IQDEV\ElasticSearch\Search\Request;
final class CriteriaToRequest
{
public function __construct(
private readonly Configuration $configuration,
) {
}
public function fromCriteria(Criteria $criteria): Request
{
$builder = new CriteriaRequestBuilder($this->configuration, $criteria);
$builder->build();
return $builder->getRequest();
}
}
<?php
declare(strict_types=1);
namespace IQDEV\ElasticSearch\Converter\Request\Filter;
use IQDEV\ElasticSearch\Esable;
use IQDEV\ElasticSearch\Criteria\Filter\Collection\FilterGroupCollection;
use IQDEV\ElasticSearch\Criteria\Filter\LogicOperator;
use IQDEV\ElasticSearch\Search\BoolQuery\Query;
use IQDEV\ElasticSearch\Search\Nested;
abstract class AbstractFilterQuery
{
protected Query $query;
public function __construct() {
$this->query = new Query();
}
protected function setFilterByLogic(Query $query, LogicOperator $logicOperator, Esable $filter): void
{
match ($logicOperator) {
LogicOperator::AND => $query->getFilter()->add($filter),
LogicOperator::OR => $query->getShould()->add($filter),
LogicOperator::NOT => $query->getMustNot()->add($filter),
};
}
abstract public function getQuery(FilterGroupCollection $filterGroupCollection, array $exclude): Query|Nested;
}
<?php
declare(strict_types=1);
namespace IQDEV\ElasticSearch\Converter\Request\Filter;
use IQDEV\ElasticSearch\Converter\Request\Filter\Type\KeywordFilterType;
use IQDEV\ElasticSearch\Converter\Request\Filter\Type\RangeFilterType;
use IQDEV\ElasticSearch\Criteria\Filter\Collection\FilterGroupCollection;
use IQDEV\ElasticSearch\Criteria\Filter\Filter;
use IQDEV\ElasticSearch\Search\BoolQuery\Query;
use IQDEV\ElasticSearch\Search\BoolQuery\Terms;
use IQDEV\ElasticSearch\Search\Nested;
class NestedFilter extends AbstractFilterQuery
{
public const NESTED_RANGE_PATH = 'search_data.number_facet';
public const NESTED_KEYWORD_PATH = 'search_data.keyword_facet';
public function getQuery(FilterGroupCollection $filterGroupCollection, array $exclude): Nested
{
$query = new Query();
$keywordFiltersGroup = $filterGroupCollection->getKeywordFilters($exclude);
foreach ($keywordFiltersGroup as $keywordFilter) {
/** @var Filter $keywordFilter */
$esableFilter = new KeywordFilterType($keywordFilter);
$this->setFilterByLogic($query, $filterGroupCollection->getLogicOperator(), $this->getNested($esableFilter, self::NESTED_KEYWORD_PATH));
}
$rangeFilterGroup = $filterGroupCollection->getRangeFilters($exclude);
$rangesFilter = RangeFilterType::getFiltersByOneProperty($rangeFilterGroup);
foreach ($rangesFilter as $filterGroup) {
/** @var FilterGroupCollection $filterGroup */
$esableFilter = new RangeFilterType($filterGroup);
$this->setFilterByLogic($query, $filterGroupCollection->getLogicOperator(), $this->getNested($esableFilter, self::NESTED_RANGE_PATH));
}
return (new Nested())
->setPath('search_data')
->setQuery($query);
}
private function getNested(RangeFilterType|KeywordFilterType $filter, string $path): Nested
{
$cloneFilter = clone $filter;
$nested = new Nested();
$query = new Query();
$query->getFilter()->add(new Terms($path . '.facet_code', $cloneFilter->getField()));
$cloneFilter->setField($path . '.facet_value');
$query->getFilter()->add($cloneFilter->getEsable());
$nested
->setPath($path)
->setQuery($query);
return $nested;
}
}
<?php
declare(strict_types=1);
namespace IQDEV\ElasticSearch\Converter\Request\Filter;
use IQDEV\ElasticSearch\Converter\Request\Filter\Type\KeywordFilterType;
use IQDEV\ElasticSearch\Converter\Request\Filter\Type\RangeFilterType;
use IQDEV\ElasticSearch\Criteria\Filter\Collection\FilterGroupCollection;
use IQDEV\ElasticSearch\Criteria\Filter\Filter;
use IQDEV\ElasticSearch\Search\BoolQuery\Query;
class PropertyFilter extends AbstractFilterQuery
{
public function getQuery(FilterGroupCollection $filterGroupCollection, array $exclude = []): Query
{
$query = new Query();
$keywordFiltersGroup = $filterGroupCollection->getKeywordFilters($exclude);
foreach ($keywordFiltersGroup as $keywordFilter) {
/** @var Filter $keywordFilter */
$esableFilter = new KeywordFilterType($keywordFilter);
$this->setFilterByLogic($query, $filterGroupCollection->getLogicOperator(), $esableFilter->getEsable());
}
$rangeFilterGroup = $filterGroupCollection->getRangeFilters($exclude);
$rangesFilter = RangeFilterType::getFiltersByOneProperty($rangeFilterGroup);
foreach ($rangesFilter as $filterGroup) {
/** @var FilterGroupCollection $filterGroup */
$esableFilter = new RangeFilterType($filterGroup);
$this->setFilterByLogic($query, $filterGroupCollection->getLogicOperator(), $esableFilter->getEsable());
}
return $query;
}
}
<?php
declare(strict_types=1);
namespace IQDEV\ElasticSearch\Converter\Request\Filter\Type;
class AbstractFilterType
{
protected string $field = '';
public function getField(): string
{
return $this->field;
}
public function setField(string $field): self
{
$this->field = $field;
return $this;
}
}