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