From 0608997cb149a03c6b70e3121be308e1aa087f66 Mon Sep 17 00:00:00 2001 From: Pavel Piligrimov <p.piligrimov@iqdev.digital> Date: Fri, 13 Jan 2023 11:29:40 +0500 Subject: [PATCH] updates for iqdev/search-dc --- .../Config/BaseConfiguration.php | 7 +- src/ElasticSearch/Config/MappingValidator.php | 23 + src/ElasticSearch/Config/product.mappings.php | 33 +- src/ElasticSearch/Config/product.settings.php | 64 +++ src/ElasticSearch/Configuration.php | 4 +- .../Converter/CriteriaToEsRequest.php | 398 ++++++++++++++++++ .../Converter/EsResponseToResult.php | 156 +++++++ src/ElasticSearch/Document/Document.php | 2 +- .../Document/ProductDocument.php | 74 +++- .../Domain/SearchResultFactory.php | 124 ------ src/ElasticSearch/Domain/SearchService.php | 183 -------- src/ElasticSearch/Facet/Facet.php | 2 +- src/ElasticSearch/Facet/FacetCategory.php | 8 +- src/ElasticSearch/Facet/FacetCollection.php | 2 +- src/ElasticSearch/Facet/FacetKeyword.php | 15 +- src/ElasticSearch/Facet/FacetNumber.php | 17 +- src/ElasticSearch/Helper/ArrayHelper.php | 30 ++ src/ElasticSearch/Indexer/AddIndex.php | 37 ++ .../Indexer/BaseIndexProvider.php | 31 +- src/ElasticSearch/Indexer/BulkIndex.php | 41 ++ src/ElasticSearch/Indexer/DeleteIndex.php | 31 ++ .../Indexer/EsHelperEndpoint.php | 180 ++++++++ src/ElasticSearch/Indexer/Index.php | 30 +- src/ElasticSearch/Indexer/IndexProvider.php | 39 +- src/ElasticSearch/Indexer/IndexRunner.php | 82 +++- src/ElasticSearch/Indexer/UpdateIndex.php | 39 ++ src/ElasticSearch/Order/Order.php | 24 ++ src/ElasticSearch/Order/OrderAscType.php | 8 + src/ElasticSearch/Order/OrderCollection.php | 26 ++ src/ElasticSearch/Order/OrderDescType.php | 8 + src/ElasticSearch/Order/OrderField.php | 7 + .../Order/OrderKeywordProperty.php | 29 ++ .../Order/OrderNumberProperty.php | 29 ++ src/ElasticSearch/Order/OrderType.php | 13 + src/ElasticSearch/Search/Aggs/Aggs.php | 48 +-- .../Search/Aggs/AggsCollection.php | 18 +- .../Search/Aggs/AggsFacetStats.php | 31 ++ ...ggsKeyWordFacet.php => AggsFacetTerms.php} | 3 +- .../Search/Aggs/AggsNumberFacet.php | 31 -- .../Search/Aggs/BoolQueryCollection.php | 19 - .../Aggs/{ExtremumTerms.php => Stats.php} | 8 +- src/ElasticSearch/Search/Aggs/Terms.php | 9 +- .../Search/BoolQuery/BoolQueryCollection.php | 4 +- .../Search/BoolQuery/FilterKeywordFacet.php | 7 +- .../Search/BoolQuery/FilterNumberFacet.php | 40 +- src/ElasticSearch/Search/BoolQuery/Query.php | 39 +- .../Search/BoolQuery/RangeTerms.php | 38 -- src/ElasticSearch/Search/BoolQuery/Stats.php | 34 ++ src/ElasticSearch/Search/BoolQuery/Terms.php | 12 +- src/ElasticSearch/Search/Nested.php | 9 +- src/ElasticSearch/Search/Pagination.php | 4 +- src/ElasticSearch/Search/Request.php | 78 ++-- src/ElasticSearch/Search/Sorting.php | 28 -- src/ElasticSearch/Search/Sorting/Sorting.php | 10 - .../Search/Sorting/SortingCollection.php | 20 - .../Search/Sorting/SortingField.php | 18 - .../Search/Sorting/SortingFieldsPair.php | 8 - .../Search/Sorting/SortingPair.php | 20 - .../Sorting/SortingPropertyKeywordPair.php | 27 -- .../Sorting/SortingPropertyNumberPair.php | 27 -- src/ElasticSearch/SearchService.php | 44 ++ 61 files changed, 1641 insertions(+), 789 deletions(-) create mode 100644 src/ElasticSearch/Config/MappingValidator.php create mode 100644 src/ElasticSearch/Config/product.settings.php create mode 100644 src/ElasticSearch/Converter/CriteriaToEsRequest.php create mode 100644 src/ElasticSearch/Converter/EsResponseToResult.php delete mode 100644 src/ElasticSearch/Domain/SearchResultFactory.php delete mode 100644 src/ElasticSearch/Domain/SearchService.php create mode 100644 src/ElasticSearch/Helper/ArrayHelper.php create mode 100644 src/ElasticSearch/Indexer/AddIndex.php create mode 100644 src/ElasticSearch/Indexer/BulkIndex.php create mode 100644 src/ElasticSearch/Indexer/DeleteIndex.php create mode 100644 src/ElasticSearch/Indexer/EsHelperEndpoint.php create mode 100644 src/ElasticSearch/Indexer/UpdateIndex.php create mode 100644 src/ElasticSearch/Order/Order.php create mode 100644 src/ElasticSearch/Order/OrderAscType.php create mode 100644 src/ElasticSearch/Order/OrderCollection.php create mode 100644 src/ElasticSearch/Order/OrderDescType.php create mode 100644 src/ElasticSearch/Order/OrderField.php create mode 100644 src/ElasticSearch/Order/OrderKeywordProperty.php create mode 100644 src/ElasticSearch/Order/OrderNumberProperty.php create mode 100644 src/ElasticSearch/Order/OrderType.php create mode 100644 src/ElasticSearch/Search/Aggs/AggsFacetStats.php rename src/ElasticSearch/Search/Aggs/{AggsKeyWordFacet.php => AggsFacetTerms.php} (96%) delete mode 100644 src/ElasticSearch/Search/Aggs/AggsNumberFacet.php delete mode 100644 src/ElasticSearch/Search/Aggs/BoolQueryCollection.php rename src/ElasticSearch/Search/Aggs/{ExtremumTerms.php => Stats.php} (79%) delete mode 100644 src/ElasticSearch/Search/BoolQuery/RangeTerms.php create mode 100644 src/ElasticSearch/Search/BoolQuery/Stats.php delete mode 100644 src/ElasticSearch/Search/Sorting.php delete mode 100644 src/ElasticSearch/Search/Sorting/Sorting.php delete mode 100644 src/ElasticSearch/Search/Sorting/SortingCollection.php delete mode 100644 src/ElasticSearch/Search/Sorting/SortingField.php delete mode 100644 src/ElasticSearch/Search/Sorting/SortingFieldsPair.php delete mode 100644 src/ElasticSearch/Search/Sorting/SortingPair.php delete mode 100644 src/ElasticSearch/Search/Sorting/SortingPropertyKeywordPair.php delete mode 100644 src/ElasticSearch/Search/Sorting/SortingPropertyNumberPair.php create mode 100644 src/ElasticSearch/SearchService.php diff --git a/src/ElasticSearch/Config/BaseConfiguration.php b/src/ElasticSearch/Config/BaseConfiguration.php index 3ed3ea6..73e561c 100644 --- a/src/ElasticSearch/Config/BaseConfiguration.php +++ b/src/ElasticSearch/Config/BaseConfiguration.php @@ -15,4 +15,9 @@ class BaseConfiguration implements Configuration { return include __DIR__.'/product.mappings.php'; } -} \ No newline at end of file + + public function getSettings(): array + { + return include __DIR__.'/product.settings.php'; + } +} diff --git a/src/ElasticSearch/Config/MappingValidator.php b/src/ElasticSearch/Config/MappingValidator.php new file mode 100644 index 0000000..e8d6b30 --- /dev/null +++ b/src/ElasticSearch/Config/MappingValidator.php @@ -0,0 +1,23 @@ +<?php + +namespace IQDEV\ElasticSearch\Config; + +use IQDEV\ElasticSearch\Configuration; + +class MappingValidator +{ + /** + * Проверка ÑущеÑÑ‚Ð²Ð¾Ð²Ð°Ñ Ð¿Ð¾Ð»Ñ Ð² опиÑании индекÑа + * + * @param Configuration $configuration + * @param string $property + * + * @return bool + */ + public static function isPropertyExists(Configuration $configuration, string $property): bool + { + $properties = array_keys($configuration->getMapping()['properties'] ?? []); + + return in_array($property, $properties, true); + } +} diff --git a/src/ElasticSearch/Config/product.mappings.php b/src/ElasticSearch/Config/product.mappings.php index 2d73e84..bd51078 100644 --- a/src/ElasticSearch/Config/product.mappings.php +++ b/src/ElasticSearch/Config/product.mappings.php @@ -9,8 +9,21 @@ return [ 'full_search_content' => [ 'type' => 'text', ], + 'suggest_search_content' => [ + 'type' => 'text', + 'analyzer' => 'autocomplete', + 'search_analyzer' => 'standard', + ], 'category_id' => [ - 'type' => 'keyword', + 'type' => 'keyword', + 'index' => false, + ], + 'rating' => [ + 'type' => 'double', + 'index' => false, + ], + 'popular' => [ + 'type' => 'double', 'index' => false, ], 'search_data' => [ @@ -20,28 +33,28 @@ return [ 'type' => 'nested', 'properties' => [ 'facet_code' => [ - 'type' => 'keyword', - 'index' => true + 'type' => 'keyword', + 'index' => true ], 'facet_value' => [ - 'type' => 'keyword', - 'index' => true - ], + 'type' => 'keyword', + 'index' => true + ] ] ], 'number_facet' => [ 'type' => 'nested', 'properties' => [ 'facet_code' => [ - 'type' => 'keyword', - 'index' => true + 'type' => 'keyword', + 'index' => true ], 'facet_value' => [ - 'type' => 'double' + 'type' => 'double' ] ] ] ] ] ], -]; \ No newline at end of file +]; diff --git a/src/ElasticSearch/Config/product.settings.php b/src/ElasticSearch/Config/product.settings.php new file mode 100644 index 0000000..6d176f5 --- /dev/null +++ b/src/ElasticSearch/Config/product.settings.php @@ -0,0 +1,64 @@ +<?php + +return [ + 'index' => [ + 'analysis' => [ + 'char_filter' => [ + 'eCharFilter' => [ + 'type' => 'mapping', + 'mappings' => [ + 'Ð=>Е', + 'Ñ‘=>е', + ',=>.', + ] + ] + ], + 'analyzer' => [ + 'search_analyzer' => [ + 'type' => 'custom', + 'tokenizer' => 'standard', + 'filter' => [ + 'lowercase', + 'russian_stemmer', + 'russian_stop', + 'synonym_filter', + 'shingle', + ], + 'char_filter' => ['eCharFilter'] + ], + 'autocomplete' => [ + 'type' => 'custom', + 'tokenizer' => 'standard', + 'filter' => [ + 'lowercase', + 'autocomplete_filter' + ] + ] + ], + 'filter' => [ + 'shingle' => [ + 'type' => 'shingle', + 'min_shingle_size' => 2, + 'max_shingle_size' => 3 + ], + 'russian_stop' => [ + 'type' => 'stop', + ], + 'russian_stemmer' => [ + 'type' => 'stemmer', + 'language' => 'russian' + ], + 'synonym_filter' => [ + 'type' => 'synonym_graph', + 'expand' => false, + 'synonyms' => [] + ], + 'autocomplete_filter' => [ + 'type' => 'edge_ngram', + 'min_gram' => 1, + 'max_gram' => 10, + ] + ] + ], + ] +]; \ No newline at end of file diff --git a/src/ElasticSearch/Configuration.php b/src/ElasticSearch/Configuration.php index ef48509..da7c7d2 100644 --- a/src/ElasticSearch/Configuration.php +++ b/src/ElasticSearch/Configuration.php @@ -7,4 +7,6 @@ interface Configuration public function getIndexName(): string; public function getMapping(): array; -} \ No newline at end of file + + public function getSettings(): array; +} diff --git a/src/ElasticSearch/Converter/CriteriaToEsRequest.php b/src/ElasticSearch/Converter/CriteriaToEsRequest.php new file mode 100644 index 0000000..2c8e274 --- /dev/null +++ b/src/ElasticSearch/Converter/CriteriaToEsRequest.php @@ -0,0 +1,398 @@ +<?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; + } +} diff --git a/src/ElasticSearch/Converter/EsResponseToResult.php b/src/ElasticSearch/Converter/EsResponseToResult.php new file mode 100644 index 0000000..e1e89c4 --- /dev/null +++ b/src/ElasticSearch/Converter/EsResponseToResult.php @@ -0,0 +1,156 @@ +<?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); + } +} diff --git a/src/ElasticSearch/Document/Document.php b/src/ElasticSearch/Document/Document.php index 9922ebc..62944ad 100644 --- a/src/ElasticSearch/Document/Document.php +++ b/src/ElasticSearch/Document/Document.php @@ -6,4 +6,4 @@ use IQDEV\ElasticSearch\Esable; interface Document extends Esable { -} \ No newline at end of file +} diff --git a/src/ElasticSearch/Document/ProductDocument.php b/src/ElasticSearch/Document/ProductDocument.php index d6071e8..87752e8 100644 --- a/src/ElasticSearch/Document/ProductDocument.php +++ b/src/ElasticSearch/Document/ProductDocument.php @@ -2,24 +2,38 @@ namespace IQDEV\ElasticSearch\Document; +use IQDEV\ElasticSearch\Config\MappingValidator; +use IQDEV\ElasticSearch\Configuration; use IQDEV\ElasticSearch\Facet\FacetCategory; use IQDEV\ElasticSearch\Facet\FacetCollection; +use IQDEV\ElasticSearch\Helper\ArrayHelper; class ProductDocument implements Document { + private array $properties = []; private FacetCollection $keywordFacets; private FacetCollection $numberFacets; - private ?string $fullSearchContent = null; - private array $info; + private ?string $searchContent = null; + private array $info = []; + private bool $skipEmpty = false; private FacetCategory $categoryFacet; - public function __construct(FacetCategory $categoryFacet, array $info = []) + + public function __construct( + FacetCategory $categoryFacet + ) { $this->keywordFacets = new FacetCollection(); $this->numberFacets = new FacetCollection(); + $this->categoryFacet = $categoryFacet; - $this->info = $info; + + } + + public static function create(FacetCategory $categoryFacet): self + { + return new self($categoryFacet); } /** @@ -47,11 +61,45 @@ class ProductDocument implements Document } /** - * @param string|null $fullSearchContent + * @param string|null $searchContent */ - public function setFullSearchContent(?string $fullSearchContent): void + public function setSearchContent(?string $searchContent): void { - $this->fullSearchContent = $fullSearchContent; + $this->searchContent = $searchContent; + } + + public function setAdditionData(array $info): self + { + $this->info = $info; + return $this; + } + + /** + * УÑтановка Ð·Ð½Ð°Ñ‡ÐµÐ½Ð¸Ñ ÑвойÑтва документа индекÑа по параметрам конфигурации. + * Имеет приоритет по Ñравнению Ñ Ð²Ñ‹Ð·Ð¾Ð²Ð°Ð¼Ð¸ функций Ð´Ð»Ñ ÑƒÑтановки данных. + * + * @param Configuration $configuration + * @param string $property + * @param $value + * + * @return $this + */ + public function setByConfiguration(Configuration $configuration, string $property, $value): self + { + if (!MappingValidator::isPropertyExists($configuration, $property)) { + throw new \InvalidArgumentException('Property ' . $property . ' doesnt exist'); + } + + $this->properties[$property] = $value; + + return $this; + } + + public function skipEmpty(bool $skipEmpty = false): self + { + $this->skipEmpty = $skipEmpty; + + return $this; } public function es(): array @@ -65,10 +113,16 @@ class ProductDocument implements Document 'data' => $this->info ]; - if ($this->fullSearchContent) { - $document['full_search_content'] = $this->fullSearchContent; + if (isset($this->searchContent)) { + $document['full_search_content'] = $this->searchContent; + $document['suggest_search_content'] = $this->searchContent; + } + + $result = array_replace_recursive($document, $this->properties); + if (true === $this->skipEmpty) { + $result = ArrayHelper::array_filter_recursive($result); } - return $document; + return $result; } } diff --git a/src/ElasticSearch/Domain/SearchResultFactory.php b/src/ElasticSearch/Domain/SearchResultFactory.php deleted file mode 100644 index 363abe4..0000000 --- a/src/ElasticSearch/Domain/SearchResultFactory.php +++ /dev/null @@ -1,124 +0,0 @@ -<?php - -namespace IQDEV\ElasticSearch\Domain; - -use IQDEV\Search\Document\Document; -use IQDEV\Search\Facet\Facet; -use IQDEV\Search\Facet\FacetItemList; -use IQDEV\Search\Facet\FacetItemRange; -use IQDEV\Search\Facet\RangeFacetType; -use IQDEV\Search\Result; -use IQDEV\ElasticSearch\Search\Request; -use Elastic\Elasticsearch\Response\Elasticsearch; -use Http\Promise\Promise; -use IQDEV\Search\Facet\ListFacetType; - -final class SearchResultFactory -{ - /** - * @param Elasticsearch|Promise $response - * @param Request $request - * @return Result - */ - public static function createFromResponse($response, Request $request): Result - { - $result = new Result(); - - $data = json_decode($response->getBody(), true); - - if (isset($data['hits']['hits'])) { - foreach ($data['hits']['hits'] as $hit) { - $result->hits->add(new Document($hit['_id'], $hit)); - } - $result->numProduct = (int)$data['hits']['total']['value']; - } - - if ($request->getPagination()) { - $result->numPages = ceil($result->numProduct / $request->getPagination()->size); - } - - 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 $filtredBucket) { - foreach ($filtredBucket['agg_keyword_facet_value']['buckets'] as $values) { - if ($filtredBucket['key'] === $code) { - $bucketsFiltered[$code][$values['key']] = $values; - } - } - } - } - } - - foreach ($buckets as $bucket) { - $code = $bucket['key']; - $valueBucket = $bucket['agg_keyword_facet_value']['buckets']; - - $facet = new Facet(new ListFacetType, $code); - - foreach ($valueBucket as $value) { - $count = !empty($bucketsFiltered) || $result->numProduct === 0 ? 0 : $value['doc_count']; - if (isset($bucketsFiltered[$code][$value['key']])) { - $count = $bucketsFiltered[$code][$value['key']]['doc_count']; - } - $facet->items->add(FacetItemList::createFromValue($value['key'], $count)); - } - - $result->facets->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']] = [ - 'data' => $bucket, - 'min' => $bucket['agg_number_facet_value']['min'], - 'max' => $bucket['agg_number_facet_value']['max'] - ]; - } - } - - foreach ($buckets as $bucket) { - $code = $bucket['key']; - - $facet = new Facet(new RangeFacetType, $code); - $count = !empty($bucketsFiltered) || $result->numProduct === 0 ? 0 : $bucket['doc_count']; - $selectedMin = $selectedMax = null; - if (isset($bucketsFiltered[$code])) { - $count = $bucketsFiltered[$code]['data']['doc_count']; - $selectedMin = $bucketsFiltered[$code]['min']; - $selectedMax = $bucketsFiltered[$code]['max']; - } - - $facet->items->add(FacetItemRange::createFromRange( - $bucket['agg_number_facet_value']['min'], - $bucket['agg_number_facet_value']['max'], - $count, - $selectedMin, - $selectedMax - ) - ); - - $result->facets->add($facet); - } - } - - return $result; - } -} \ No newline at end of file diff --git a/src/ElasticSearch/Domain/SearchService.php b/src/ElasticSearch/Domain/SearchService.php deleted file mode 100644 index ffa88f7..0000000 --- a/src/ElasticSearch/Domain/SearchService.php +++ /dev/null @@ -1,183 +0,0 @@ -<?php - -namespace IQDEV\ElasticSearch\Domain; - -use IQDEV\ElasticSearch\Search\Aggs\Aggs; -use IQDEV\ElasticSearch\Search\Aggs\AggsKeyWordFacet; -use IQDEV\ElasticSearch\Search\Aggs\AggsNumberFacet; -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 Elastic\Elasticsearch\Client; -use IQDEV\Search\{Filter\Filter, - Filter\FilterCollection, - Filter\FilterKeyword, - Filter\FilterCategory, - Filter\FilterNumber, - Query as DQuery, - Result, - SearchService as DomainSearchService, - Sorting\SortingFieldPair as DSortingFieldPair, - Sorting\SortingPropertyKeywordPair as DSortingPropertyKeywordPair, - Sorting\SortingPropertyNumberPair as DSortingPropertyNumberPair}; -use IQDEV\ElasticSearch\Search\Sorting\SortingCollection; -use IQDEV\ElasticSearch\Search\Sorting\SortingFieldsPair; -use IQDEV\ElasticSearch\Search\Sorting\SortingPropertyKeywordPair; -use IQDEV\ElasticSearch\Search\Sorting\SortingPropertyNumberPair; - -final class SearchService implements DomainSearchService -{ - private Client $esClient; - private string $sIndex; - - public function __construct(Client $esClient, string $sIndex = 'product-test') - { - $this->esClient = $esClient; - $this->sIndex = $sIndex; - } - - public function search(DQuery $q): Result - { - $request = new Request(); - $commonQuery = new Query(); - - if ($q->filters) { - foreach ($q->filters as $filter) { - /** @var Filter $filter */ - if ($filter instanceof FilterCategory) { - $request->getQuery()->must( - (new Terms('category_id', $filter->value)) - ); - break; - } - } - $commonQuery = $this->getQuery($q->filters); - } - $commonFilter = clone $commonQuery; - $commonFilter->setType(Query::TYPE_FILTER); - - if ($q->pagination) { - $request->setPagination( - new Pagination($q->pagination->limit, $q->pagination->page) - ); - } - - if ($q->sorting && !$q->sorting->isEmpty()) { - $oSortingCollection = new SortingCollection(); - foreach ($q->sorting as $sorting) { - if ($sorting instanceof DSortingFieldPair) { - $oSortingCollection->add(new SortingFieldsPair($sorting->by, $sorting->direction)); - } - - if ($sorting instanceof DSortingPropertyKeywordPair) { - $oSortingCollection->add(new SortingPropertyKeywordPair($sorting->by, $sorting->direction)); - } - - if ($sorting instanceof DSortingPropertyNumberPair) { - $oSortingCollection->add(new SortingPropertyNumberPair($sorting->by, $sorting->direction)); - } - } - $request->setSorting($oSortingCollection); - } - - if ($q->query) { - $request->addMatch('full_search_content', ['query' => $q->query]); - } - - $getKey = static fn (string $type, string $name): string => sprintf('%s_facet_%s', $type, $name); - if ($commonQuery->isEmpty() === false) { - foreach ($q->filters as $filter) { - /** @var Filter $filter */ - if ($filter instanceof FilterCategory || $filter instanceof FilterNumber) { - continue; - } - - $oFilters = new FilterCollection(); - - if ($filter instanceof FilterKeyword) { - foreach ($q->filters as $filter2) { - if (!($filter2 instanceof Filter) || $filter2 instanceof FilterCategory) { - continue; - } - - if ($filter->key() === $filter2->key()) { - continue; - } - $oFilters->add($filter2); - } - } - - $aggsFiltered = new Aggs($getKey('keyword', $filter->key())); - $aggsFiltered->addAggs( - AggsKeyWordFacet::create( - 'agg_special', - 'keyword_facet' - ) - ); - - if (false === $oFilters->isEmpty()) { - $aggsFiltered->setQuery($this->getQuery($oFilters)); - } else { - $aggsFiltered->setNested((new Nested())->setPath('search_data')); - } - - $request->getAggs()->add($aggsFiltered); - } - - $nestedFilter = new Nested(); - $nestedFilter->setPath('search_data') - ->setQuery($commonQuery); - - $request->getPostFilter()->filter($nestedFilter); - - $aggsKeywordFiltered = new Aggs('keyword_facet_filtered'); - $aggsKeywordFiltered->addAggs(AggsKeyWordFacet::create('all_keyword_facet_filtered', 'keyword_facet')) - ->setQuery($commonFilter); - $request->getAggs()->add($aggsKeywordFiltered); - - $aggsNumberFiltered = new Aggs('number_facet_filtered'); - $aggsNumberFiltered->addAggs(AggsNumberFacet::create('all_number_facet_filtered', 'number_facet')) - ->setQuery($commonFilter); - $request->getAggs()->add($aggsNumberFiltered); - } - - $aggsKeyword = AggsKeyWordFacet::create('keyword_facet', 'keyword_facet'); - $request->getAggs()->add($aggsKeyword); - $aggsNumber = AggsNumberFacet::create('number_facet', 'number_facet'); - $request->getAggs()->add($aggsNumber); - - $response = $this->esClient->search( - [ - 'index' => $this->sIndex, - 'body' => $request->es(), - ] - ); - - - return SearchResultFactory::createFromResponse($response, $request); - } - - private function getQuery(FilterCollection $filters): Query - { - $commonQuery = new Query(); - foreach ($filters as $filter) { - if ($filter instanceof FilterNumber) { - $oFacet = new FilterNumberFacet($filter->key, $filter->min, $filter->max); - $commonQuery->filter($oFacet); - continue; - } - - if ($filter instanceof FilterKeyword) { - $oFacet = new FilterKeywordFacet($filter->key, $filter->value); - $commonQuery->filter($oFacet); - continue; - } - } - - return $commonQuery; - } -} diff --git a/src/ElasticSearch/Facet/Facet.php b/src/ElasticSearch/Facet/Facet.php index 11b2a39..4cce190 100644 --- a/src/ElasticSearch/Facet/Facet.php +++ b/src/ElasticSearch/Facet/Facet.php @@ -6,4 +6,4 @@ use IQDEV\ElasticSearch\Esable; interface Facet extends Esable { -} \ No newline at end of file +} diff --git a/src/ElasticSearch/Facet/FacetCategory.php b/src/ElasticSearch/Facet/FacetCategory.php index 48bb468..6e286b1 100644 --- a/src/ElasticSearch/Facet/FacetCategory.php +++ b/src/ElasticSearch/Facet/FacetCategory.php @@ -6,21 +6,17 @@ use IQDEV\ElasticSearch\Esable; final class FacetCategory implements Esable { - public string $category; + private string $category; - /** - * @param string $category - */ public function __construct(string $category) { $this->category = $category; } - public function es(): array { return [ - 'category_id' => $this->category + 'category_id' => $this->category, ]; } } diff --git a/src/ElasticSearch/Facet/FacetCollection.php b/src/ElasticSearch/Facet/FacetCollection.php index 6d82851..e84af39 100644 --- a/src/ElasticSearch/Facet/FacetCollection.php +++ b/src/ElasticSearch/Facet/FacetCollection.php @@ -16,4 +16,4 @@ final class FacetCollection extends AbstractCollection implements Esable { return array_map(static fn(Facet $facet) => $facet->es(), $this->toArray()); } -} \ No newline at end of file +} diff --git a/src/ElasticSearch/Facet/FacetKeyword.php b/src/ElasticSearch/Facet/FacetKeyword.php index 448f427..fc43162 100644 --- a/src/ElasticSearch/Facet/FacetKeyword.php +++ b/src/ElasticSearch/Facet/FacetKeyword.php @@ -4,17 +4,14 @@ namespace IQDEV\ElasticSearch\Facet; final class FacetKeyword implements Facet { - public string $key; - public $value; - + private string $key; + /** @var string|string[] */ + private $value; + /** - * @param string $key * @param string|string[] $value */ - public function __construct( - string $key, - $value - ) + public function __construct(string $key, $value) { $this->key = $key; $this->value = $value; @@ -27,4 +24,4 @@ final class FacetKeyword implements Facet 'facet_value' => $this->value, ]; } -} \ No newline at end of file +} diff --git a/src/ElasticSearch/Facet/FacetNumber.php b/src/ElasticSearch/Facet/FacetNumber.php index 947d4cd..953dfca 100644 --- a/src/ElasticSearch/Facet/FacetNumber.php +++ b/src/ElasticSearch/Facet/FacetNumber.php @@ -4,17 +4,10 @@ namespace IQDEV\ElasticSearch\Facet; final class FacetNumber implements Facet { - public string $key; - public float $value; - - /** - * @param string $key - * @param float $value - */ - public function __construct( - string $key, - float $value - ) + private string $key; + private float $value; + + public function __construct(string $key, float $value) { $this->key = $key; $this->value = $value; @@ -27,4 +20,4 @@ final class FacetNumber implements Facet 'facet_value' => $this->value, ]; } -} \ No newline at end of file +} diff --git a/src/ElasticSearch/Helper/ArrayHelper.php b/src/ElasticSearch/Helper/ArrayHelper.php new file mode 100644 index 0000000..e050625 --- /dev/null +++ b/src/ElasticSearch/Helper/ArrayHelper.php @@ -0,0 +1,30 @@ +<?php + +namespace IQDEV\ElasticSearch\Helper; + +class ArrayHelper +{ + /** + * Recursively filter an array + * + * @param array $array + * @param callable|null $callback + * + * @return array + */ + public static function array_filter_recursive(array $array, ?callable $callback = null): array + { + $array = is_callable($callback) ? array_filter($array, $callback) : array_filter($array); + foreach ($array as &$value) { + if (is_array($value)) { + $value = call_user_func([__CLASS__, __FUNCTION__], $value, $callback); + + if (!empty($value)) { + $value = self::array_filter_recursive($value, $callback); + } + } + } + + return $array; + } +} \ No newline at end of file diff --git a/src/ElasticSearch/Indexer/AddIndex.php b/src/ElasticSearch/Indexer/AddIndex.php new file mode 100644 index 0000000..2c51748 --- /dev/null +++ b/src/ElasticSearch/Indexer/AddIndex.php @@ -0,0 +1,37 @@ +<?php + +namespace IQDEV\ElasticSearch\Indexer; + +use IQDEV\ElasticSearch\Esable; + +final class AddIndex implements Index +{ + private string $name; + private Esable $body; + private ?string $id; + + public function __construct( + string $name, + Esable $body, + ?string $id = null + ) + { + $this->name = $name; + $this->body = $body; + $this->id = $id; + } + + public function es(): array + { + $es = [ + 'index' => $this->name, + 'body' => $this->body->es(), + ]; + + if ($this->id) { + $es['id'] = $this->id; + } + + return $es; + } +} diff --git a/src/ElasticSearch/Indexer/BaseIndexProvider.php b/src/ElasticSearch/Indexer/BaseIndexProvider.php index d8bafd1..2a5aee2 100644 --- a/src/ElasticSearch/Indexer/BaseIndexProvider.php +++ b/src/ElasticSearch/Indexer/BaseIndexProvider.php @@ -11,6 +11,8 @@ use IQDEV\ElasticSearch\Facet\FacetNumber; class BaseIndexProvider implements IndexProvider { private array $products; + private ?int $size = null; + private ?int $limit = null; private Configuration $configuration; public function __construct($products, $configuration) @@ -22,7 +24,8 @@ class BaseIndexProvider implements IndexProvider public function get(): \Generator { foreach ($this->products as $product) { - $document = new ProductDocument(new FacetCategory($product['category']), $product['data'] ?? []); + $document = new ProductDocument(new FacetCategory($product['category'])); + $document->setAdditionData($product['data'] ?? []); foreach ($product['properties'] as $type => $values) { foreach ($values as $key => $prop) { if ($type === 'number') { @@ -32,13 +35,33 @@ class BaseIndexProvider implements IndexProvider } } } - $document->setFullSearchContent($product['name']); + $document->setSearchContent($product['name']); - yield new Index( + yield new AddIndex( $this->configuration->getIndexName(), $document, $product['id'] ); } } -} \ No newline at end of file + + public function setBatchSize(int $size): void + { + $this->size = $size; + } + + public function getBatchSize(): ?int + { + return $this->size; + } + + public function setLimit(int $limit): void + { + $this->limit = $limit; + } + + public function getLimit(): ?int + { + return $this->limit; + } +} diff --git a/src/ElasticSearch/Indexer/BulkIndex.php b/src/ElasticSearch/Indexer/BulkIndex.php new file mode 100644 index 0000000..94134a2 --- /dev/null +++ b/src/ElasticSearch/Indexer/BulkIndex.php @@ -0,0 +1,41 @@ +<?php + +namespace IQDEV\ElasticSearch\Indexer; + +use IQDEV\ElasticSearch\Esable; + +final class BulkIndex implements Index +{ + private string $name; + private Esable $body; + private ?string $id; + + public function __construct( + string $name, + Esable $body, + ?string $id = null + ) + { + $this->name = $name; + $this->body = $body; + $this->id = $id; + } + + public function es(): array + { + $es = [ + [ + 'index' => [ + '_index' => $this->name + ] + ] + ]; + if ($this->id) { + $es[0]['index']['_id'] = $this->id; + } + + $es[] = $this->body->es(); + + return $es; + } +} diff --git a/src/ElasticSearch/Indexer/DeleteIndex.php b/src/ElasticSearch/Indexer/DeleteIndex.php new file mode 100644 index 0000000..df4b049 --- /dev/null +++ b/src/ElasticSearch/Indexer/DeleteIndex.php @@ -0,0 +1,31 @@ +<?php + +namespace IQDEV\ElasticSearch\Indexer; + +final class DeleteIndex implements Index +{ + private string $name; + private ?string $id; + + public function __construct( + string $name, + ?string $id = null + ) + { + $this->name = $name; + $this->id = $id; + } + + public function es(): array + { + $es = [ + 'index' => $this->name + ]; + + if ($this->id) { + $es['id'] = $this->id; + } + + return $es; + } +} diff --git a/src/ElasticSearch/Indexer/EsHelperEndpoint.php b/src/ElasticSearch/Indexer/EsHelperEndpoint.php new file mode 100644 index 0000000..bc37f33 --- /dev/null +++ b/src/ElasticSearch/Indexer/EsHelperEndpoint.php @@ -0,0 +1,180 @@ +<?php + +namespace IQDEV\ElasticSearch\Indexer; + +use Elastic\Elasticsearch\Client; +use Elastic\Elasticsearch\Exception\ClientResponseException; +use Elastic\Elasticsearch\Exception\ContentTypeException; +use Elastic\Elasticsearch\Exception\MissingParameterException; +use Elastic\Elasticsearch\Exception\ServerResponseException; +use Elastic\Elasticsearch\Response\Elasticsearch; +use Elastic\Elasticsearch\Traits\EndpointTrait; +use IQDEV\ElasticSearch\Configuration; +use Psr\Log\LoggerInterface; + +final class EsHelperEndpoint +{ + use EndpointTrait; + + private Client $esClient; + private Configuration $configuration; + + private LoggerInterface $logger; + + public function __construct( + Client $esClient, + Configuration $configuration, + LoggerInterface $logger + ) + { + $this->esClient = $esClient; + $this->configuration = $configuration; + $this->logger = $logger; + } + + public function isIndexExists(): bool + { + $response = $this->esClient + ->indices() + ->exists( + [ + 'index' => $this->configuration->getIndexName(), + ] + ); + + return $response instanceof Elasticsearch && true === $response->asBool(); + } + + /** + * Создание индекÑа + * + * @return void + * + * @throws \Elastic\Elasticsearch\Exception\ClientResponseException + * @throws \Elastic\Elasticsearch\Exception\MissingParameterException + * @throws \Elastic\Elasticsearch\Exception\ServerResponseException + */ + public function create(): void + { + if (false === $this->isIndexExists()) { + $this->logger->info(sprintf('Index %s was created', $this->configuration->getIndexName())); + $this->esClient->indices()->create( + [ + 'index' => $this->configuration->getIndexName(), + 'body' => [ + 'mappings' => $this->configuration->getMapping(), + 'settings' => $this->configuration->getSettings(), + ], + ] + ); + } + } + + /** + * Обновление конфигурации индекÑа + * + * @throws ContentTypeException + */ + public function reconfigurate(): void + { + $this->esClient->sendRequest( + $this->createRequest( + 'POST', + '/' . $this->encode($this->configuration->getIndexName()) . '/_close', + [ + 'Accept' => 'application/json', + 'Content-Type' => 'application/json', + ], + [], + ) + ); + $this->esClient->sendRequest( + $this->createRequest( + 'PUT', + '/' . $this->encode($this->configuration->getIndexName()) . '/_settings', + [ + 'Accept' => 'application/json', + 'Content-Type' => 'application/json', + ], + $this->configuration->getSettings(), + ) + ); + + $this->esClient->sendRequest( + $this->createRequest( + 'PUT', + '/' . $this->encode($this->configuration->getIndexName()) . '/_mapping', + [ + 'Accept' => 'application/json', + 'Content-Type' => 'application/json', + ], + $this->configuration->getMapping(), + ) + ); + + $this->esClient->sendRequest( + $this->createRequest( + 'POST', + '/' . $this->encode($this->configuration->getIndexName()) . '/_open', + [ + 'Accept' => 'application/json', + 'Content-Type' => 'application/json', + ], + [], + ) + ); + $this->logger->info(sprintf('Index %s was reconfigurated', $this->configuration->getIndexName())); + } + + /** + * ÐŸÐ¾Ð»Ð½Ð°Ñ Ð¾Ñ‡Ð¸Ñтка документов индекÑа + * + * @throws ContentTypeException + */ + public function clear(): void + { + if ($this->isIndexExists()) { + $this->esClient->sendRequest( + $this->createRequest( + 'POST', + '/' . $this->encode($this->configuration->getIndexName()) . '/_delete_by_query', + [ + 'Accept' => 'application/json', + 'Content-Type' => 'application/json', + ], + '{"query": {"match_all": {}}}' + ) + ); + $this->logger->info(sprintf('Index %s was cleared', $this->configuration->getIndexName())); + } else { + $this->logger->error(sprintf('Index %s does not exists', $this->configuration->getIndexName())); + } + } + + /** + * Удаление индекÑа + * + * @return void + * + * @throws ClientResponseException + * @throws MissingParameterException + * @throws ServerResponseException + */ + public function delete(): void + { + if ($this->isIndexExists()) { + $response = $this->esClient + ->indices() + ->delete( + [ + 'index' => $this->configuration->getIndexName(), + ] + ); + if (($response instanceof Elasticsearch) && false === $response->asBool()) { + $this->logger->info(sprintf('Index %s was deleted', $this->configuration->getIndexName())); + } else { + $this->logger->error(sprintf('Index %s was not deleted', $this->configuration->getIndexName())); + } + } + } +} \ No newline at end of file diff --git a/src/ElasticSearch/Indexer/Index.php b/src/ElasticSearch/Indexer/Index.php index 30d0cea..f6138cc 100644 --- a/src/ElasticSearch/Indexer/Index.php +++ b/src/ElasticSearch/Indexer/Index.php @@ -4,33 +4,7 @@ namespace IQDEV\ElasticSearch\Indexer; use IQDEV\ElasticSearch\Esable; -final class Index implements Esable +interface Index extends Esable { - private string $name; - private Esable $body; - private ?string $id; - - public function __construct( - string $name, - Esable $body, - ?string $id = null - ) { - $this->name = $name; - $this->body = $body; - $this->id = $id; - } - public function es(): array - { - $es = [ - 'index' => $this->name, - 'body' => $this->body->es(), - ]; - - if ($this->id) { - $es['id'] = $this->id; - } - - return $es; - } -} +} \ No newline at end of file diff --git a/src/ElasticSearch/Indexer/IndexProvider.php b/src/ElasticSearch/Indexer/IndexProvider.php index 38db55d..83dc1a3 100644 --- a/src/ElasticSearch/Indexer/IndexProvider.php +++ b/src/ElasticSearch/Indexer/IndexProvider.php @@ -4,5 +4,42 @@ namespace IQDEV\ElasticSearch\Indexer; interface IndexProvider { + /** + * Итерационное получение Ñлемнтов Ð´Ð»Ñ Ð¾Ð±Ð½Ð¾Ð²Ð»ÐµÐ½Ð¸Ñ + * + * @return \Generator|Index[] + */ public function get(): \Generator; -} \ No newline at end of file + + /** + * УÑтановка размера пакета Ð´Ð»Ñ Ð¿ÐµÑ€ÐµÐ´Ð°Ñ‡Ð¸Ð² elasticsearch + * + * @param int $size + * + * @return void + */ + public function setBatchSize(int $size): void; + + /** + * Получение размера пакета Ð´Ð»Ñ Ð¿ÐµÑ€ÐµÐ´Ð°Ñ‡Ð¸Ð² elasticsearch + * + * @return int|null + */ + public function getBatchSize(): ?int; + + /** + * УÑтановка лимита на количеÑтво обрабатываемых данных Ð´Ð»Ñ Ð¸Ð½Ð´ÐµÐºÑации за один раз + * + * @param int $limit + * + * @return void + */ + public function setLimit(int $limit): void; + + /** + * Получение лимита на количеÑтво обрабатываемых данных Ð´Ð»Ñ Ð¸Ð½Ð´ÐµÐºÑации за один раз + * + * @return int|null + */ + public function getLimit(): ?int; +} diff --git a/src/ElasticSearch/Indexer/IndexRunner.php b/src/ElasticSearch/Indexer/IndexRunner.php index 572f2f1..8ac968b 100644 --- a/src/ElasticSearch/Indexer/IndexRunner.php +++ b/src/ElasticSearch/Indexer/IndexRunner.php @@ -4,40 +4,82 @@ namespace IQDEV\ElasticSearch\Indexer; use Elastic\Elasticsearch\Client; use IQDEV\ElasticSearch\Configuration; +use Psr\Log\LoggerInterface; final class IndexRunner { private Client $esClient; - private Configuration $configuration; + private EsHelperEndpoint $helper; public function __construct( - Client $esClient, - Configuration $configuration - ) { + Client $esClient, + Configuration $configuration, + LoggerInterface $logger + ) + { $this->esClient = $esClient; - $this->configuration = $configuration; + $this->helper = new EsHelperEndpoint($esClient, $configuration, $logger); } public function run(IndexProvider $indexProvider) { - if ($this->esClient->indices()->exists(['index' => $this->configuration->getIndexName()])->asBool() === false) { - $this->esClient->indices()->create( - [ - 'index' => $this->configuration->getIndexName(), - 'body' => [ - 'mappings' => $this->configuration->getMapping(), - ], - ] - ); - } + $this->helper->create(); + + if ($indexProvider->getBatchSize() !== null && $indexProvider->getBatchSize() > 0) { + $counter = 0; + $params = ['body' => []]; + foreach ($indexProvider->get() as $index) { + if ($index instanceof DeleteIndex) { + if (!empty($params['body'])) { + $this->esClient->bulk($params); + $params = ['body' => []]; + $counter = 0; + } + $this->esClient->delete($index->es()); + continue; + } + + if ($index instanceof UpdateIndex) { + if (!empty($params['body'])) { + $this->esClient->bulk($params); + $params = ['body' => []]; + $counter = 0; + } + $this->esClient->update($index->es()); + continue; + } + + if (!$index instanceof BulkIndex) { + continue; + } + $esIndex = $index->es(); + foreach ($esIndex as $indexItem) { + $params['body'][] = $indexItem; + } - foreach ($indexProvider->get() as $index) { - if (!$index instanceof Index) { - continue; + if (++$counter >= $indexProvider->getBatchSize()) { + $this->esClient->bulk($params); + $params = ['body' => []]; + $counter = 0; + } } - $esIndex = $index->es(); - $this->esClient->index($esIndex); + if (!empty($params['body'])) { + $this->esClient->bulk($params); + } + } else { + foreach ($indexProvider->get() as $index) { + if ($index instanceof DeleteIndex) { + $this->esClient->delete($index->es()); + continue; + } + + if (!($index instanceof AddIndex) && !($index instanceof UpdateIndex)) { + continue; + } + + $this->esClient->index($index->es()); + } } } } diff --git a/src/ElasticSearch/Indexer/UpdateIndex.php b/src/ElasticSearch/Indexer/UpdateIndex.php new file mode 100644 index 0000000..5057292 --- /dev/null +++ b/src/ElasticSearch/Indexer/UpdateIndex.php @@ -0,0 +1,39 @@ +<?php + +namespace IQDEV\ElasticSearch\Indexer; + +use IQDEV\ElasticSearch\Esable; + +final class UpdateIndex implements Index +{ + private string $name; + private Esable $body; + private ?string $id; + + public function __construct( + string $name, + Esable $body, + ?string $id = null + ) + { + $this->name = $name; + $this->body = $body; + $this->id = $id; + } + + public function es(): array + { + $es = [ + 'index' => $this->name, + 'body' => [ + 'doc' => $this->body->es() + ] + ]; + + if ($this->id) { + $es['id'] = $this->id; + } + + return $es; + } +} diff --git a/src/ElasticSearch/Order/Order.php b/src/ElasticSearch/Order/Order.php new file mode 100644 index 0000000..b23f067 --- /dev/null +++ b/src/ElasticSearch/Order/Order.php @@ -0,0 +1,24 @@ +<?php + +namespace IQDEV\ElasticSearch\Order; + +use IQDEV\ElasticSearch\Esable; + +abstract class Order implements Esable +{ + public string $by; + public OrderType $direction; + public array $properties; + + public function __construct(string $by, OrderType $direction, array $properties = []) + { + $this->by = $by; + $this->direction = $direction; + $this->properties = $properties; + } + + public function es(): array + { + return array_merge([$this->by => $this->direction::getType()], $this->properties); + } +} diff --git a/src/ElasticSearch/Order/OrderAscType.php b/src/ElasticSearch/Order/OrderAscType.php new file mode 100644 index 0000000..69b8f08 --- /dev/null +++ b/src/ElasticSearch/Order/OrderAscType.php @@ -0,0 +1,8 @@ +<?php + +namespace IQDEV\ElasticSearch\Order; + +class OrderAscType extends OrderType +{ + protected static string $code = 'asc'; +} diff --git a/src/ElasticSearch/Order/OrderCollection.php b/src/ElasticSearch/Order/OrderCollection.php new file mode 100644 index 0000000..3be74b6 --- /dev/null +++ b/src/ElasticSearch/Order/OrderCollection.php @@ -0,0 +1,26 @@ +<?php + +namespace IQDEV\ElasticSearch\Order; + +use IQDEV\ElasticSearch\Esable; +use Ramsey\Collection\AbstractCollection; + +class OrderCollection extends AbstractCollection implements Esable +{ + + /** + * @inheritDoc + */ + public function getType(): string + { + return Order::class; + } + + /** + * @inheritDoc + */ + public function es(): array + { + return array_map(static fn(Order $order) => $order->es(), $this->toArray()); + } +} diff --git a/src/ElasticSearch/Order/OrderDescType.php b/src/ElasticSearch/Order/OrderDescType.php new file mode 100644 index 0000000..775fa3c --- /dev/null +++ b/src/ElasticSearch/Order/OrderDescType.php @@ -0,0 +1,8 @@ +<?php + +namespace IQDEV\ElasticSearch\Order; + +class OrderDescType extends OrderType +{ + protected static string $code = 'desc'; +} diff --git a/src/ElasticSearch/Order/OrderField.php b/src/ElasticSearch/Order/OrderField.php new file mode 100644 index 0000000..ba56650 --- /dev/null +++ b/src/ElasticSearch/Order/OrderField.php @@ -0,0 +1,7 @@ +<?php + +namespace IQDEV\ElasticSearch\Order; + +class OrderField extends Order +{ +} diff --git a/src/ElasticSearch/Order/OrderKeywordProperty.php b/src/ElasticSearch/Order/OrderKeywordProperty.php new file mode 100644 index 0000000..c490062 --- /dev/null +++ b/src/ElasticSearch/Order/OrderKeywordProperty.php @@ -0,0 +1,29 @@ +<?php + +namespace IQDEV\ElasticSearch\Order; + +class OrderKeywordProperty extends Order +{ + public function es(): array + { + $order = [ + 'order' => $this->direction::getType(), + 'nested' => [ + 'path' => 'search_data', + 'filter' => [ + 'bool' => [ + 'must' => [ + 'term' => [ + 'search_data.keyword_facet.facet_code' => $this->by, + ] + ], + ], + ], + ], + ]; + $order = array_merge($order, $this->properties); + return [ + 'search_data.keyword_facet.facet_value' => $order, + ]; + } +} diff --git a/src/ElasticSearch/Order/OrderNumberProperty.php b/src/ElasticSearch/Order/OrderNumberProperty.php new file mode 100644 index 0000000..befc546 --- /dev/null +++ b/src/ElasticSearch/Order/OrderNumberProperty.php @@ -0,0 +1,29 @@ +<?php + +namespace IQDEV\ElasticSearch\Order; + +class OrderNumberProperty extends Order +{ + public function es(): array + { + $order = [ + 'order' => $this->direction::getType(), + 'nested' => [ + 'path' => 'search_data', + 'filter' => [ + 'bool' => [ + 'must' => [ + 'term' => [ + 'search_data.number_facet.facet_code' => $this->by, + ] + ], + ], + ], + ], + ]; + $order = array_merge($order, $this->properties); + return [ + 'search_data.number_facet.facet_value' => $order, + ]; + } +} diff --git a/src/ElasticSearch/Order/OrderType.php b/src/ElasticSearch/Order/OrderType.php new file mode 100644 index 0000000..979e079 --- /dev/null +++ b/src/ElasticSearch/Order/OrderType.php @@ -0,0 +1,13 @@ +<?php + +namespace IQDEV\ElasticSearch\Order; + +abstract class OrderType +{ + protected static string $code; + + public static function getType(): string + { + return static::$code; + } +} diff --git a/src/ElasticSearch/Search/Aggs/Aggs.php b/src/ElasticSearch/Search/Aggs/Aggs.php index ecb9e35..e17f74e 100644 --- a/src/ElasticSearch/Search/Aggs/Aggs.php +++ b/src/ElasticSearch/Search/Aggs/Aggs.php @@ -12,7 +12,7 @@ final class Aggs implements Esable private ?Query $query = null; private ?Nested $nested = null; private ?Terms $terms = null; - private ?ExtremumTerms $extremumTerms = null; + private ?Stats $stats = null; private string $key; public function __construct(string $key) @@ -20,63 +20,45 @@ final class Aggs implements Esable $this->key = $key; } - /** - * @param Aggs|null $aggs - * @return Aggs - */ - public function addAggs(?Aggs $aggs): self + public function addAggs(Aggs $aggs): self { - if ($this->aggs === null) { + if (null === $this->aggs) { $this->aggs = new AggsCollection(); } $this->aggs->add($aggs); + return $this; } - /** - * @param Query|null $query - * @return Aggs - */ public function setQuery(?Query $query): self { $this->query = $query; + return $this; } - /** - * @param Nested|null $nested - * @return Aggs - */ public function setNested(?Nested $nested): self { $this->nested = $nested; + return $this; } - /** - * @param Terms|null $terms - * @return Aggs - */ public function setTerms(?Terms $terms): self { $this->terms = $terms; + return $this; } - /** - * @param ExtremumTerms|null $terms - * @return Aggs - */ - public function setExtremumTerms(?ExtremumTerms $terms): self + public function setStats(?Stats $stats): self { - $this->extremumTerms = $terms; + $this->stats = $stats; + return $this; } - /** - * @return string - */ public function getKey(): string { return $this->key; @@ -90,7 +72,7 @@ final class Aggs implements Esable $agg['aggs'] = array_merge($agg, $this->aggs->es()['aggs']); } - if (isset($this->query) && $this->query->isEmpty() === false) { + if ($this->query && false === $this->query->isEmpty()) { $agg['filter'] = $this->query->es()[$this->query->getType()]; } @@ -101,11 +83,11 @@ final class Aggs implements Esable if ($this->terms) { $agg['terms'] = $this->terms->es()['terms']; } - - if ($this->extremumTerms) { - $agg = array_merge($agg, $this->extremumTerms->es()); + + if ($this->stats) { + $agg['stats'] = $this->stats->es()['stats']; } return $agg; } -} \ No newline at end of file +} diff --git a/src/ElasticSearch/Search/Aggs/AggsCollection.php b/src/ElasticSearch/Search/Aggs/AggsCollection.php index f7a8006..90ca24d 100644 --- a/src/ElasticSearch/Search/Aggs/AggsCollection.php +++ b/src/ElasticSearch/Search/Aggs/AggsCollection.php @@ -3,22 +3,28 @@ namespace IQDEV\ElasticSearch\Search\Aggs; use IQDEV\ElasticSearch\Esable; -use Ramsey\Collection\AbstractCollection; -final class AggsCollection extends AbstractCollection implements Esable +final class AggsCollection implements Esable { - public function getType(): string + /** + * @var Aggs[] + */ + private array $aggs = []; + + public function add(Aggs $aggs): self { - return Aggs::class; + $this->aggs[] = $aggs; + + return $this; } public function es(): array { $aggs = []; - foreach ($this as $agg) { + foreach ($this->aggs as $agg) { $aggs[$agg->getKey()] = $agg->es(); } return ['aggs' => $aggs]; } -} \ No newline at end of file +} diff --git a/src/ElasticSearch/Search/Aggs/AggsFacetStats.php b/src/ElasticSearch/Search/Aggs/AggsFacetStats.php new file mode 100644 index 0000000..d44574c --- /dev/null +++ b/src/ElasticSearch/Search/Aggs/AggsFacetStats.php @@ -0,0 +1,31 @@ +<?php + +namespace IQDEV\ElasticSearch\Search\Aggs; + +use IQDEV\ElasticSearch\Search\Nested; + +final class AggsFacetStats +{ + public static function create(string $code, string $facet, string $path = 'search_data'): Aggs + { + $aggNumberFacet = new Aggs($code); + $nested = new Nested(); + $nested->setPath($path . '.' . $facet); + $aggNumberFacet->setNested($nested); + + $aggNumberFacetCode = new Aggs("agg_{$facet}_code"); + $aggNumberFacetCode->setTerms( + (new Terms("{$path}.{$facet}.facet_code")) + ->setSize(250) + ); + $aggKeywordFacetValue = new Aggs("agg_{$facet}_value"); + $aggKeywordFacetValue->setStats( + new Stats("{$path}.{$facet}.facet_value") + ); + + $aggNumberFacetCode->addAggs($aggKeywordFacetValue); + $aggNumberFacet->addAggs($aggNumberFacetCode); + + return $aggNumberFacet; + } +} diff --git a/src/ElasticSearch/Search/Aggs/AggsKeyWordFacet.php b/src/ElasticSearch/Search/Aggs/AggsFacetTerms.php similarity index 96% rename from src/ElasticSearch/Search/Aggs/AggsKeyWordFacet.php rename to src/ElasticSearch/Search/Aggs/AggsFacetTerms.php index 482fa16..3f53766 100644 --- a/src/ElasticSearch/Search/Aggs/AggsKeyWordFacet.php +++ b/src/ElasticSearch/Search/Aggs/AggsFacetTerms.php @@ -4,7 +4,7 @@ namespace IQDEV\ElasticSearch\Search\Aggs; use IQDEV\ElasticSearch\Search\Nested; -final class AggsKeyWordFacet +final class AggsFacetTerms { public static function create(string $code, string $facet, string $path = 'search_data'): Aggs { @@ -18,7 +18,6 @@ final class AggsKeyWordFacet (new Terms("{$path}.{$facet}.facet_code")) ->setSize(250) ); - $aggKeywordFacetValue = new Aggs("agg_{$facet}_value"); $aggKeywordFacetValue->setTerms( (new Terms("{$path}.{$facet}.facet_value")) diff --git a/src/ElasticSearch/Search/Aggs/AggsNumberFacet.php b/src/ElasticSearch/Search/Aggs/AggsNumberFacet.php deleted file mode 100644 index 16d0b01..0000000 --- a/src/ElasticSearch/Search/Aggs/AggsNumberFacet.php +++ /dev/null @@ -1,31 +0,0 @@ -<?php - -namespace IQDEV\ElasticSearch\Search\Aggs; - -use IQDEV\ElasticSearch\Search\Nested; - -final class AggsNumberFacet -{ - public static function create(string $code, string $facet, string $path = 'search_data'): Aggs - { - $aggFacet = new Aggs($code); - $nested = new Nested(); - $nested->setPath($path . '.' . $facet); - $aggFacet->setNested($nested); - - $aggFacetCode = new Aggs("agg_{$facet}_code"); - $aggFacetCode->setTerms( - (new Terms("{$path}.{$facet}.facet_code")) - ); - - $aggFacetValue = new Aggs("agg_{$facet}_value"); - $aggFacetValue->setExtremumTerms( - (new ExtremumTerms("{$path}.{$facet}.facet_value")) - ); - $aggFacetCode->addAggs($aggFacetValue); - - $aggFacet->addAggs($aggFacetCode); - - return $aggFacet; - } -} diff --git a/src/ElasticSearch/Search/Aggs/BoolQueryCollection.php b/src/ElasticSearch/Search/Aggs/BoolQueryCollection.php deleted file mode 100644 index 536f76d..0000000 --- a/src/ElasticSearch/Search/Aggs/BoolQueryCollection.php +++ /dev/null @@ -1,19 +0,0 @@ -<?php - -namespace IQDEV\ElasticSearch\Search\Aggs; - -use IQDEV\ElasticSearch\Esable; -use Ramsey\Collection\AbstractCollection; - -final class BoolQueryCollection extends AbstractCollection implements Esable -{ - public function getType(): string - { - return Esable::class; - } - - public function es(): array - { - return array_map(static fn(Esable $facet) => $facet->es(), $this->toArray()); - } -} \ No newline at end of file diff --git a/src/ElasticSearch/Search/Aggs/ExtremumTerms.php b/src/ElasticSearch/Search/Aggs/Stats.php similarity index 79% rename from src/ElasticSearch/Search/Aggs/ExtremumTerms.php rename to src/ElasticSearch/Search/Aggs/Stats.php index 9de2a5c..344eaf5 100644 --- a/src/ElasticSearch/Search/Aggs/ExtremumTerms.php +++ b/src/ElasticSearch/Search/Aggs/Stats.php @@ -2,16 +2,12 @@ namespace IQDEV\ElasticSearch\Search\Aggs; - use IQDEV\ElasticSearch\Esable; -final class ExtremumTerms implements Esable +final class Stats implements Esable { private string $field; - /** - * @param string $field - */ public function __construct(string $field) { $this->field = $field; @@ -25,4 +21,4 @@ final class ExtremumTerms implements Esable ], ]; } -} \ No newline at end of file +} diff --git a/src/ElasticSearch/Search/Aggs/Terms.php b/src/ElasticSearch/Search/Aggs/Terms.php index 582d069..20d53b7 100644 --- a/src/ElasticSearch/Search/Aggs/Terms.php +++ b/src/ElasticSearch/Search/Aggs/Terms.php @@ -2,7 +2,6 @@ namespace IQDEV\ElasticSearch\Search\Aggs; - use IQDEV\ElasticSearch\Esable; final class Terms implements Esable @@ -10,9 +9,6 @@ final class Terms implements Esable private array $options = []; private string $field; - /** - * @param string $field - */ public function __construct(string $field) { $this->field = $field; @@ -21,14 +17,15 @@ final class Terms implements Esable public function setSize(int $size): self { $this->options['size'] = $size; + return $this; } public function es(): array { - $data = $this->options; + $data = $this->options; $data['field'] = $this->field; return ['terms' => $data]; } -} \ No newline at end of file +} diff --git a/src/ElasticSearch/Search/BoolQuery/BoolQueryCollection.php b/src/ElasticSearch/Search/BoolQuery/BoolQueryCollection.php index d3d901d..7cd83f8 100644 --- a/src/ElasticSearch/Search/BoolQuery/BoolQueryCollection.php +++ b/src/ElasticSearch/Search/BoolQuery/BoolQueryCollection.php @@ -14,6 +14,6 @@ final class BoolQueryCollection extends AbstractCollection implements Esable public function es(): array { - return array_map(static fn(Esable $facet) => $facet->es(), $this->toArray()); + return array_map(static fn (Esable $facet) => $facet->es(), $this->toArray()); } -} \ No newline at end of file +} diff --git a/src/ElasticSearch/Search/BoolQuery/FilterKeywordFacet.php b/src/ElasticSearch/Search/BoolQuery/FilterKeywordFacet.php index cfa5e50..3e20d97 100644 --- a/src/ElasticSearch/Search/BoolQuery/FilterKeywordFacet.php +++ b/src/ElasticSearch/Search/BoolQuery/FilterKeywordFacet.php @@ -5,17 +5,16 @@ namespace IQDEV\ElasticSearch\Search\BoolQuery; use IQDEV\ElasticSearch\Esable; use IQDEV\ElasticSearch\Search\Nested; - final class FilterKeywordFacet implements Esable { public string $key; - + /** @var string|string[] */ public $value; public function __construct(string $key, $value) { - $this->key = $key; + $this->key = $key; $this->value = $value; } @@ -36,4 +35,4 @@ final class FilterKeywordFacet implements Esable return $nested->es(); } -} \ No newline at end of file +} diff --git a/src/ElasticSearch/Search/BoolQuery/FilterNumberFacet.php b/src/ElasticSearch/Search/BoolQuery/FilterNumberFacet.php index 178cded..b150931 100644 --- a/src/ElasticSearch/Search/BoolQuery/FilterNumberFacet.php +++ b/src/ElasticSearch/Search/BoolQuery/FilterNumberFacet.php @@ -4,20 +4,18 @@ namespace IQDEV\ElasticSearch\Search\BoolQuery; use IQDEV\ElasticSearch\Esable; use IQDEV\ElasticSearch\Search\Nested; - +use IQDEV\Search\Filter\FilterOperator; final class FilterNumberFacet implements Esable { public string $key; - public float $min = 0; - public float $max = PHP_INT_MAX; + public array $conditions; - public function __construct(string $key, float $min, float $max) + public function __construct(string $key, array $conditions) { $this->key = $key; - $this->min = $min; - $this->max = $max; + $this->conditions = $conditions; } public function es(): array @@ -28,8 +26,32 @@ final class FilterNumberFacet implements Esable $query = new Query(); $query - ->filter(new Terms($path . '.facet_code', $this->key)) - ->filter(new RangeTerms($path . '.facet_value', $this->min, $this->max)); + ->filter(new Stats($path.'.facet_code', $this->key)); + + $conditions = []; + foreach ($this->conditions as $operator => $value) { + switch ($operator) { + case FilterOperator::GTE: + $key = 'gte'; + break; + case FilterOperator::LTE: + $key = 'lte'; + break; + case FilterOperator::GT: + $key = 'gt'; + break; + case FilterOperator::LT: + $key = 'lt'; + break; + default: + $key = null; + break; + } + if (isset($key)) { + $conditions[$key] = $value; + } + } + $query->filter(new Stats($path.'.facet_value', $conditions)); $nested ->setPath($path) @@ -37,4 +59,4 @@ final class FilterNumberFacet implements Esable return $nested->es(); } -} \ No newline at end of file +} diff --git a/src/ElasticSearch/Search/BoolQuery/Query.php b/src/ElasticSearch/Search/BoolQuery/Query.php index ba44f6e..7a235b1 100644 --- a/src/ElasticSearch/Search/BoolQuery/Query.php +++ b/src/ElasticSearch/Search/BoolQuery/Query.php @@ -5,16 +5,15 @@ namespace IQDEV\ElasticSearch\Search\BoolQuery; use IQDEV\ElasticSearch\Esable; use IQDEV\ElasticSearch\Search\Nested; - final class Query implements Esable { protected BoolQueryCollection $must; protected BoolQueryCollection $filter; protected BoolQueryCollection $should; protected BoolQueryCollection $mustNot; - + protected string $type; - + public const TYPE_QUERY = 'query'; public const TYPE_FILTER = 'filter'; @@ -24,9 +23,21 @@ final class Query implements Esable $this->filter = new BoolQueryCollection(); $this->should = new BoolQueryCollection(); $this->mustNot = new BoolQueryCollection(); + $this->match = new BoolQueryCollection(); $this->type = self::TYPE_QUERY; } + /** + * @param Terms|Nested $item + * @return $this + */ + public function match($item): self + { + $this->match->add($item); + + return $this; + } + /** * @param Terms|Nested $item * @return $this @@ -34,24 +45,28 @@ final class Query implements Esable public function must($item): self { $this->must->add($item); + return $this; } public function filter(Esable $item): self { $this->filter->add($item); + return $this; } public function should(Esable $item): self { $this->should->add($item); + return $this; } public function mustNot(Esable $item): self { $this->mustNot->add($item); + return $this; } @@ -62,7 +77,7 @@ final class Query implements Esable && $this->filter->isEmpty() && $this->should->isEmpty(); } - + public function setType(string $type): self { switch($type) { @@ -84,26 +99,30 @@ final class Query implements Esable public function es(): array { $bool = []; - if ($this->filter->isEmpty() === false) { + if (false === $this->filter->isEmpty()) { $bool['filter'] = $this->filter->es(); } - if ($this->must->isEmpty() === false) { + if (false === $this->must->isEmpty()) { $bool['must'] = $this->must->es(); } - if ($this->mustNot->isEmpty() === false) { + if (false === $this->mustNot->isEmpty()) { $bool['must_not'] = $this->mustNot->es(); } - if ($this->should->isEmpty() === false) { + if (false === $this->should->isEmpty()) { $bool['should'] = $this->should->es(); } + if (false === $this->match->isEmpty()) { + $bool['match'] = $this->match->es(); + } + return [ $this->type => [ 'bool' => $bool, - ] + ], ]; } -} \ No newline at end of file +} diff --git a/src/ElasticSearch/Search/BoolQuery/RangeTerms.php b/src/ElasticSearch/Search/BoolQuery/RangeTerms.php deleted file mode 100644 index 8491942..0000000 --- a/src/ElasticSearch/Search/BoolQuery/RangeTerms.php +++ /dev/null @@ -1,38 +0,0 @@ -<?php - -namespace IQDEV\ElasticSearch\Search\BoolQuery; - - -use IQDEV\ElasticSearch\Esable; - -final class RangeTerms implements Esable -{ - private string $key; - - private float $min; - private float $max; - - /** - * @param string $key - * @param float $min - * @param float $max - */ - public function __construct(string $key, float $min, float $max) - { - $this->key = $key; - $this->min = $min; - $this->max = $max; - } - - public function es(): array - { - return [ - 'range' => [ - $this->key => [ - 'gte' => $this->min, - 'lte' => $this->max - ] - ] - ]; - } -} \ No newline at end of file diff --git a/src/ElasticSearch/Search/BoolQuery/Stats.php b/src/ElasticSearch/Search/BoolQuery/Stats.php new file mode 100644 index 0000000..c0e54f7 --- /dev/null +++ b/src/ElasticSearch/Search/BoolQuery/Stats.php @@ -0,0 +1,34 @@ +<?php + +namespace IQDEV\ElasticSearch\Search\BoolQuery; + +use IQDEV\ElasticSearch\Esable; + +final class Stats implements Esable +{ + private string $key; + /** + * @var string|float|string[]|float[] + */ + private $value; + + /** + * @param string|float|string[]|float[] $value + */ + public function __construct(string $key, $value) + { + $this->key = $key; + $this->value = $value; + } + + public function es(): array + { + $keyStats = is_array($this->value) ? 'range' : 'term'; + + return [ + $keyStats => [ + $this->key => $this->value, + ], + ]; + } +} diff --git a/src/ElasticSearch/Search/BoolQuery/Terms.php b/src/ElasticSearch/Search/BoolQuery/Terms.php index c4ad517..8f66de9 100644 --- a/src/ElasticSearch/Search/BoolQuery/Terms.php +++ b/src/ElasticSearch/Search/BoolQuery/Terms.php @@ -2,22 +2,22 @@ namespace IQDEV\ElasticSearch\Search\BoolQuery; - use IQDEV\ElasticSearch\Esable; final class Terms implements Esable { private string $key; - /** @var string|float|string[]|float[] */ + /** + * @var string|float|string[]|float[] + */ private $value; /** - * @param string $key * @param string|float|string[]|float[] $value */ public function __construct(string $key, $value) { - $this->key = $key; + $this->key = $key; $this->value = $value; } @@ -27,8 +27,8 @@ final class Terms implements Esable return [ $keyTerms => [ - $this->key => $this->value + $this->key => $this->value, ], ]; } -} \ No newline at end of file +} diff --git a/src/ElasticSearch/Search/Nested.php b/src/ElasticSearch/Search/Nested.php index f0bc805..c2e6c4d 100644 --- a/src/ElasticSearch/Search/Nested.php +++ b/src/ElasticSearch/Search/Nested.php @@ -17,15 +17,22 @@ class Nested implements Esable public function setPath(string $p): self { $this->path = $p; + return $this; } public function setQuery(Query $q): self { $this->query = $q; + return $this; } + public function getQuery(): ?Query + { + return $this->query; + } + public function es(): array { $nested = []; @@ -34,7 +41,7 @@ class Nested implements Esable $nested['path'] = $this->path; } - if (isset($this->query) && $this->query->isEmpty() === false) { + if ($this->query && false === $this->query->isEmpty()) { $nested['query'] = $this->query->es()['query']; } diff --git a/src/ElasticSearch/Search/Pagination.php b/src/ElasticSearch/Search/Pagination.php index 1135315..56832d4 100644 --- a/src/ElasticSearch/Search/Pagination.php +++ b/src/ElasticSearch/Search/Pagination.php @@ -6,8 +6,8 @@ use IQDEV\ElasticSearch\Esable; class Pagination implements Esable { - public ?int $size; - public ?int $from; + private ?int $size; + private ?int $from; public function __construct(?int $size = null, ?int $from = null) { diff --git a/src/ElasticSearch/Search/Request.php b/src/ElasticSearch/Search/Request.php index ab423a9..99eb6a0 100644 --- a/src/ElasticSearch/Search/Request.php +++ b/src/ElasticSearch/Search/Request.php @@ -3,9 +3,9 @@ namespace IQDEV\ElasticSearch\Search; use IQDEV\ElasticSearch\Esable; +use IQDEV\ElasticSearch\Order\OrderCollection; use IQDEV\ElasticSearch\Search\Aggs\AggsCollection; use IQDEV\ElasticSearch\Search\BoolQuery\Query; -use IQDEV\ElasticSearch\Search\Sorting\SortingCollection; final class Request implements Esable { @@ -13,47 +13,29 @@ final class Request implements Esable private ?Query $postFilter = null; private ?AggsCollection $aggs = null; private ?Pagination $pagination = null; - private ?SortingCollection $sorting = null; + private ?OrderCollection $sort = null; private array $match = []; + private ?array $source = null; - /** - * @param Pagination|null $pagination - * @return Request - */ public function setPagination(?Pagination $pagination): self { $this->pagination = $pagination; - return $this; - } - /** - * @param SortingCollection|null $sorting - * @return $this - */ - public function setSorting(?SortingCollection $sorting): self - { - $this->sorting = $sorting; return $this; } - /** - * @return Query - */ public function getQuery(): Query { - if ($this->query === null) { + if (null === $this->query) { $this->query = new Query(); } return $this->query; } - /** - * @return Query - */ public function getPostFilter(): Query { - if ($this->postFilter === null) { + if (null === $this->postFilter) { $this->postFilter = new Query(); } @@ -62,50 +44,58 @@ final class Request implements Esable public function getAggs(): AggsCollection { - if ($this->aggs === null) { + if (null === $this->aggs) { $this->aggs = new AggsCollection(); } return $this->aggs; } - /** - * @return Pagination|null - */ public function getPagination(): ?Pagination { return $this->pagination; } - /** - * @return SortingCollection|null - */ - public function getSorting(): ?SortingCollection + public function addMatch(string $key, array $param): self { - return $this->sorting; + $this->match[$key] = $param; + + return $this; } - public function addMatch(string $key, array $param): self + public function setSource(array $s): self { - $this->match[$key] = $param; + $this->source = $s; + return $this; } + public function getSort(): OrderCollection + { + if (null === $this->sort) { + $this->sort = new OrderCollection(); + } + + return $this->sort; + } + public function es(): array { - $request = [ - '_source' => ['id', 'data.*'] - ]; + $request = []; + + if ($this->source) { + $request['_source'] = $this->source; + } - if (isset($this->postFilter) && $this->postFilter->isEmpty() === false) { + if ($this->postFilter && false === $this->postFilter->isEmpty()) { $request['post_filter'] = $this->postFilter->es()['query']; } - if (isset($this->query) && $this->query->isEmpty() === false) { + if ($this->query && false === $this->query->isEmpty()) { $request['query'] = $this->query->es()['query']; } - if (empty($this->match) === false) { + if (false === empty($this->match)) { foreach ($this->match as $key => $value) { $request['query']['match'][$key] = $value; } @@ -118,11 +108,11 @@ final class Request implements Esable if ($this->pagination) { $request = array_merge($request, $this->pagination->es()); } - - if ($this->sorting) { - $request['sort'] = $this->sorting->es(); + + if ($this->sort) { + $request['sort'] = $this->sort->es(); } return $request; } -} \ No newline at end of file +} diff --git a/src/ElasticSearch/Search/Sorting.php b/src/ElasticSearch/Search/Sorting.php deleted file mode 100644 index 9f6bb33..0000000 --- a/src/ElasticSearch/Search/Sorting.php +++ /dev/null @@ -1,28 +0,0 @@ -<?php - -namespace IQDEV\ElasticSearch\Search; - -use IQDEV\ElasticSearch\Esable; - -class Sorting implements Esable -{ - public string $by; - public string $direction; - - public function __construct(string $by = '', string $direction = 'asc') - { - $this->by = $by; - $this->direction = $direction; - } - - public function es(): array - { - $sorting = []; - - if ($this->by) { - $sorting['sort'] = [$this->by => $this->direction]; - } - - return $sorting; - } -} \ No newline at end of file diff --git a/src/ElasticSearch/Search/Sorting/Sorting.php b/src/ElasticSearch/Search/Sorting/Sorting.php deleted file mode 100644 index 91f455a..0000000 --- a/src/ElasticSearch/Search/Sorting/Sorting.php +++ /dev/null @@ -1,10 +0,0 @@ -<?php - -namespace IQDEV\ElasticSearch\Search\Sorting; - -use IQDEV\ElasticSearch\Esable; - -interface Sorting extends Esable -{ - -} \ No newline at end of file diff --git a/src/ElasticSearch/Search/Sorting/SortingCollection.php b/src/ElasticSearch/Search/Sorting/SortingCollection.php deleted file mode 100644 index 0b55070..0000000 --- a/src/ElasticSearch/Search/Sorting/SortingCollection.php +++ /dev/null @@ -1,20 +0,0 @@ -<?php - -namespace IQDEV\ElasticSearch\Search\Sorting; - -use IQDEV\ElasticSearch\Esable; -use Ramsey\Collection\AbstractCollection; - -class SortingCollection extends AbstractCollection implements Esable -{ - - public function getType(): string - { - return Sorting::class; - } - - public function es(): array - { - return array_map(static fn(Sorting $sorting) => $sorting->es(), $this->toArray()); - } -} \ No newline at end of file diff --git a/src/ElasticSearch/Search/Sorting/SortingField.php b/src/ElasticSearch/Search/Sorting/SortingField.php deleted file mode 100644 index 39864c2..0000000 --- a/src/ElasticSearch/Search/Sorting/SortingField.php +++ /dev/null @@ -1,18 +0,0 @@ -<?php - -namespace IQDEV\ElasticSearch\Search\Sorting; - -class SortingField implements Sorting -{ - public string $by; - - public function __construct(string $by = '') - { - $this->by = $by; - } - - public function es(): array - { - return [$this->by]; - } -} \ No newline at end of file diff --git a/src/ElasticSearch/Search/Sorting/SortingFieldsPair.php b/src/ElasticSearch/Search/Sorting/SortingFieldsPair.php deleted file mode 100644 index dca8569..0000000 --- a/src/ElasticSearch/Search/Sorting/SortingFieldsPair.php +++ /dev/null @@ -1,8 +0,0 @@ -<?php - -namespace IQDEV\ElasticSearch\Search\Sorting; - -class SortingFieldsPair extends SortingPair -{ - -} \ No newline at end of file diff --git a/src/ElasticSearch/Search/Sorting/SortingPair.php b/src/ElasticSearch/Search/Sorting/SortingPair.php deleted file mode 100644 index 54d9bb0..0000000 --- a/src/ElasticSearch/Search/Sorting/SortingPair.php +++ /dev/null @@ -1,20 +0,0 @@ -<?php - -namespace IQDEV\ElasticSearch\Search\Sorting; - -abstract class SortingPair implements Sorting -{ - public string $by; - public string $direction; - - public function __construct(string $by = '', string $direction = 'asc') - { - $this->by = $by; - $this->direction = $direction; - } - - public function es(): array - { - return [$this->by => $this->direction]; - } -} \ No newline at end of file diff --git a/src/ElasticSearch/Search/Sorting/SortingPropertyKeywordPair.php b/src/ElasticSearch/Search/Sorting/SortingPropertyKeywordPair.php deleted file mode 100644 index cbcd660..0000000 --- a/src/ElasticSearch/Search/Sorting/SortingPropertyKeywordPair.php +++ /dev/null @@ -1,27 +0,0 @@ -<?php - -namespace IQDEV\ElasticSearch\Search\Sorting; - -class SortingPropertyKeywordPair extends SortingPair -{ - public function es(): array - { - return [ - 'search_data.keyword_facet.facet_value' => [ - 'order' => $this->direction, - 'nested' => [ - 'path' => 'search_data', - 'filter' => [ - 'bool' => [ - 'must' => [ - 'term' => [ - 'search_data.keyword_facet.facet_code' => $this->by, - ] - ], - ], - ], - ], - ], - ]; - } -} \ No newline at end of file diff --git a/src/ElasticSearch/Search/Sorting/SortingPropertyNumberPair.php b/src/ElasticSearch/Search/Sorting/SortingPropertyNumberPair.php deleted file mode 100644 index 2058e72..0000000 --- a/src/ElasticSearch/Search/Sorting/SortingPropertyNumberPair.php +++ /dev/null @@ -1,27 +0,0 @@ -<?php - -namespace IQDEV\ElasticSearch\Search\Sorting; - -class SortingPropertyNumberPair extends SortingPair -{ - public function es(): array - { - return [ - 'search_data.number_facet.facet_value' => [ - 'order' => $this->direction, - 'nested' => [ - 'path' => 'search_data', - 'filter' => [ - 'bool' => [ - 'must' => [ - 'term' => [ - 'search_data.number_facet.facet_code' => $this->by, - ] - ], - ], - ], - ], - ], - ]; - } -} \ No newline at end of file diff --git a/src/ElasticSearch/SearchService.php b/src/ElasticSearch/SearchService.php new file mode 100644 index 0000000..a1319b9 --- /dev/null +++ b/src/ElasticSearch/SearchService.php @@ -0,0 +1,44 @@ +<?php + +namespace IQDEV\ElasticSearch; + +use Elastic\Elasticsearch\Client; +use Elastic\Elasticsearch\Exception\ClientResponseException; +use Elastic\Elasticsearch\Exception\ServerResponseException; +use IQDEV\ElasticSearch\Converter\CriteriaToEsRequest; +use IQDEV\ElasticSearch\Converter\EsResponseToResult; +use IQDEV\Search\Criteria; +use IQDEV\Search\Result; + +class SearchService implements \IQDEV\Search\SearchService +{ + private Client $esClient; + private Configuration $configuration; + private CriteriaToEsRequest $criteriaToEsRequest; + private EsResponseToResult $esResponseToResult; + + public function __construct(Client $esClient, Configuration $configuration) + { + $this->esClient = $esClient; + $this->configuration = $configuration; + + $this->criteriaToEsRequest = new CriteriaToEsRequest(); + $this->esResponseToResult = new EsResponseToResult(); + } + + /** + * @throws ServerResponseException + * @throws ClientResponseException + */ + public function search(Criteria $criteria): Result + { + $request = $this->criteriaToEsRequest->fromCriteria($criteria); + + $response = $this->esClient->search([ + 'index' => $this->configuration->getIndexName(), + 'body' => $request->es(), + ]); + + return $this->esResponseToResult->fromResponse($response); + } +} -- GitLab