From c4803a8721fcfdf0f06e9db88b2a3cce50571f7a Mon Sep 17 00:00:00 2001 From: Dmytro Horytskyi Date: Thu, 13 Oct 2022 14:07:00 -0500 Subject: [PATCH 01/30] ACP2E-948: Product listing GraphQL query limited to total_count 10,000 products only --- .../Product/SearchCriteriaBuilder.php | 4 +- .../Products/DataProvider/ProductSearch.php | 99 +---- ...ProductCollectionSearchCriteriaBuilder.php | 77 ---- .../Model/Resolver/Products/Query/Search.php | 49 +-- .../FilterProcessor/CategoryFilter.php | 13 +- .../Product/SearchCriteriaBuilderTest.php | 2 +- .../Resolver/Products/Query/SearchTest.php | 11 - .../FilterProcessor/CategoryFilterTest.php | 62 --- .../ResourceModel/Fulltext/Collection.php | 8 +- .../Collection/SearchResultApplier.php | 22 +- .../BatchDataMapper/ProductDataMapper.php | 2 +- .../SearchAdapter/Query/Builder/Sort.php | 80 +--- .../Query/Builder/Sort/DefaultExpression.php | 60 +++ .../Query/Builder/Sort/EntityId.php | 31 ++ .../Query/Builder/Sort/ExpressionBuilder.php | 46 +++ .../Sort/ExpressionBuilderInterface.php | 24 ++ .../Query/Builder/Sort/Position.php | 95 +++++ .../Query/Builder/Sort/Relevance.php | 24 ++ .../Builder/Sort/DefaultExpressionTest.php | 139 +++++++ .../Query/Builder/Sort/EntityIdTest.php | 42 +++ .../Builder/Sort/ExpressionBuilderTest.php | 88 +++++ .../Query/Builder/Sort/PositionTest.php | 130 +++++++ .../Query/Builder/Sort/RelevanceTest.php | 40 ++ .../SearchAdapter/Query/Builder/SortTest.php | 249 ++----------- app/code/Magento/Elasticsearch/etc/di.xml | 20 + .../Model/Client/Elasticsearch.php | 22 ++ .../Elasticsearch7/SearchAdapter/Adapter.php | 53 ++- .../Unit/Model/Client/ElasticsearchTest.php | 28 +- .../Test/Unit/SearchAdapter/AdapterTest.php | 352 ++++++++++++++++++ .../Block/Product/ListProduct/SortingTest.php | 4 +- 30 files changed, 1279 insertions(+), 597 deletions(-) delete mode 100644 app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ProductSearch/ProductCollectionSearchCriteriaBuilder.php create mode 100644 app/code/Magento/Elasticsearch/SearchAdapter/Query/Builder/Sort/DefaultExpression.php create mode 100644 app/code/Magento/Elasticsearch/SearchAdapter/Query/Builder/Sort/EntityId.php create mode 100644 app/code/Magento/Elasticsearch/SearchAdapter/Query/Builder/Sort/ExpressionBuilder.php create mode 100644 app/code/Magento/Elasticsearch/SearchAdapter/Query/Builder/Sort/ExpressionBuilderInterface.php create mode 100644 app/code/Magento/Elasticsearch/SearchAdapter/Query/Builder/Sort/Position.php create mode 100644 app/code/Magento/Elasticsearch/SearchAdapter/Query/Builder/Sort/Relevance.php create mode 100644 app/code/Magento/Elasticsearch/Test/Unit/SearchAdapter/Query/Builder/Sort/DefaultExpressionTest.php create mode 100644 app/code/Magento/Elasticsearch/Test/Unit/SearchAdapter/Query/Builder/Sort/EntityIdTest.php create mode 100644 app/code/Magento/Elasticsearch/Test/Unit/SearchAdapter/Query/Builder/Sort/ExpressionBuilderTest.php create mode 100644 app/code/Magento/Elasticsearch/Test/Unit/SearchAdapter/Query/Builder/Sort/PositionTest.php create mode 100644 app/code/Magento/Elasticsearch/Test/Unit/SearchAdapter/Query/Builder/Sort/RelevanceTest.php create mode 100644 app/code/Magento/Elasticsearch7/Test/Unit/SearchAdapter/AdapterTest.php diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/Product/SearchCriteriaBuilder.php b/app/code/Magento/CatalogGraphQl/DataProvider/Product/SearchCriteriaBuilder.php index d67a50875b81..359c4df380b8 100644 --- a/app/code/Magento/CatalogGraphQl/DataProvider/Product/SearchCriteriaBuilder.php +++ b/app/code/Magento/CatalogGraphQl/DataProvider/Product/SearchCriteriaBuilder.php @@ -124,7 +124,7 @@ public function build(array $args, bool $includeAggregation): SearchCriteriaInte $this->addEntityIdSort($searchCriteria); $this->addVisibilityFilter($searchCriteria, $isSearch, !empty($args['filter'])); - $searchCriteria->setCurrentPage($args['currentPage']); + $searchCriteria->setCurrentPage($args['currentPage'] - 1); $searchCriteria->setPageSize($args['pageSize']); return $searchCriteria; @@ -168,7 +168,7 @@ private function addEntityIdSort(SearchCriteriaInterface $searchCriteria): void } $sortOrderArray[] = $this->sortOrderBuilder - ->setField('_id') + ->setField('entity_id') ->setDirection($sortDir) ->create(); $searchCriteria->setSortOrders($sortOrderArray); diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ProductSearch.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ProductSearch.php index f1d30ab942aa..5c170f428bc9 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ProductSearch.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ProductSearch.php @@ -8,18 +8,15 @@ namespace Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider; use Magento\Catalog\Api\Data\ProductSearchResultsInterfaceFactory; -use Magento\Catalog\Model\Product\Visibility; use Magento\Catalog\Model\ResourceModel\Product\Collection; use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory; use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\Product\CollectionProcessorInterface; use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\Product\CollectionPostProcessorInterface; -use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\ProductSearch\ProductCollectionSearchCriteriaBuilder; use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchResultApplierFactory; -use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchResultApplierInterface; +use Magento\Framework\Api\Search\SearchCriteriaInterfaceFactory as SearchCriteriaFactory; use Magento\Framework\Api\Search\SearchResultInterface; use Magento\Framework\Api\SearchCriteriaInterface; use Magento\Framework\Api\SearchResultsInterface; -use Magento\Framework\App\ObjectManager; use Magento\GraphQl\Model\Query\ContextInterface; /** @@ -53,14 +50,9 @@ class ProductSearch private $searchResultApplierFactory; /** - * @var ProductCollectionSearchCriteriaBuilder + * @var SearchCriteriaFactory */ - private $searchCriteriaBuilder; - - /** - * @var Visibility - */ - private $catalogProductVisibility; + private $searchCriteriaFactory; /** * @param CollectionFactory $collectionFactory @@ -68,8 +60,7 @@ class ProductSearch * @param CollectionProcessorInterface $collectionPreProcessor * @param CollectionPostProcessorInterface $collectionPostProcessor * @param SearchResultApplierFactory $searchResultsApplierFactory - * @param ProductCollectionSearchCriteriaBuilder $searchCriteriaBuilder - * @param Visibility $catalogProductVisibility + * @param SearchCriteriaFactory $searchCriteriaFactory */ public function __construct( CollectionFactory $collectionFactory, @@ -77,16 +68,14 @@ public function __construct( CollectionProcessorInterface $collectionPreProcessor, CollectionPostProcessorInterface $collectionPostProcessor, SearchResultApplierFactory $searchResultsApplierFactory, - ProductCollectionSearchCriteriaBuilder $searchCriteriaBuilder, - Visibility $catalogProductVisibility + SearchCriteriaFactory $searchCriteriaFactory ) { $this->collectionFactory = $collectionFactory; $this->searchResultsFactory = $searchResultsFactory; $this->collectionPreProcessor = $collectionPreProcessor; $this->collectionPostProcessor = $collectionPostProcessor; $this->searchResultApplierFactory = $searchResultsApplierFactory; - $this->searchCriteriaBuilder = $searchCriteriaBuilder; - $this->catalogProductVisibility = $catalogProductVisibility; + $this->searchCriteriaFactory = $searchCriteriaFactory; } /** @@ -107,75 +96,27 @@ public function getList( /** @var Collection $collection */ $collection = $this->collectionFactory->create(); - //Create a copy of search criteria without filters to preserve the results from search - $searchCriteriaForCollection = $this->searchCriteriaBuilder->build($searchCriteria); //Apply CatalogSearch results from search and join table - $this->getSearchResultsApplier( - $searchResult, - $collection, - $this->getSortOrderArray($searchCriteriaForCollection) - )->apply(); - - $collection->setFlag('search_resut_applied', true); - - $collection->setVisibility($this->catalogProductVisibility->getVisibleInSiteIds()); - $this->collectionPreProcessor->process($collection, $searchCriteriaForCollection, $attributes, $context); - $collection->load(); - $this->collectionPostProcessor->process($collection, $attributes, $context); - - $searchResults = $this->searchResultsFactory->create(); - $searchResults->setSearchCriteria($searchCriteriaForCollection); - $searchResults->setItems($collection->getItems()); - $searchResults->setTotalCount($collection->getSize()); - return $searchResults; - } - - /** - * Create searchResultApplier - * - * @param SearchResultInterface $searchResult - * @param Collection $collection - * @param array $orders - * @return SearchResultApplierInterface - */ - private function getSearchResultsApplier( - SearchResultInterface $searchResult, - Collection $collection, - array $orders - ): SearchResultApplierInterface { - return $this->searchResultApplierFactory->create( + $searchResultsApplier = $this->searchResultApplierFactory->create( [ 'collection' => $collection, 'searchResult' => $searchResult, - 'orders' => $orders ] ); - } + $searchResultsApplier->apply(); - /** - * Format sort orders into associative array - * - * E.g. ['field1' => 'DESC', 'field2' => 'ASC", ...] - * - * @param SearchCriteriaInterface $searchCriteria - * @return array - */ - private function getSortOrderArray(SearchCriteriaInterface $searchCriteria) - { - $ordersArray = []; - $sortOrders = $searchCriteria->getSortOrders(); - if (is_array($sortOrders)) { - foreach ($sortOrders as $sortOrder) { - // I am replacing _id with entity_id because in ElasticSearch _id is required for sorting by ID. - // Where as entity_id is required when using ID as the sort in $collection->load();. - // @see \Magento\CatalogGraphQl\Model\Resolver\Products\Query\Search::getResult - if ($sortOrder->getField() === '_id') { - $sortOrder->setField('entity_id'); - } - $ordersArray[$sortOrder->getField()] = $sortOrder->getDirection(); - } - } + //Empty search criteria for backward compatibility. + //Search criteria must be already applied to the search result. + $emptySearchCriteria = $this->searchCriteriaFactory->create(); + $this->collectionPreProcessor->process($collection, $emptySearchCriteria, $attributes, $context); + $collection->load(); + $this->collectionPostProcessor->process($collection, $attributes, $context); - return $ordersArray; + $searchResults = $this->searchResultsFactory->create(); + $searchResults->setSearchCriteria($searchCriteria); + $searchResults->setItems($collection->getItems()); + $searchResults->setTotalCount($searchResult->getTotalCount()); + + return $searchResults; } } diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ProductSearch/ProductCollectionSearchCriteriaBuilder.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ProductSearch/ProductCollectionSearchCriteriaBuilder.php deleted file mode 100644 index 03e8358b1ee7..000000000000 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ProductSearch/ProductCollectionSearchCriteriaBuilder.php +++ /dev/null @@ -1,77 +0,0 @@ -searchCriteriaFactory = $searchCriteriaFactory; - $this->filterBuilder = $filterBuilder; - $this->filterGroupBuilder = $filterGroupBuilder; - } - - /** - * Build searchCriteria from search for product collection - * - * @param SearchCriteriaInterface $searchCriteria - * @return SearchCriteriaInterface - */ - public function build(SearchCriteriaInterface $searchCriteria): SearchCriteriaInterface - { - //Create a copy of search criteria without filters to preserve the results from search - $searchCriteriaForCollection = $this->searchCriteriaFactory->create() - ->setSortOrders($searchCriteria->getSortOrders()) - ->setPageSize($searchCriteria->getPageSize()) - ->setCurrentPage($searchCriteria->getCurrentPage()); - - //Add category id to enable sorting by position - foreach ($searchCriteria->getFilterGroups() as $filterGroup) { - foreach ($filterGroup->getFilters() as $filter) { - if ($filter->getField() == CategoryProductLink::KEY_CATEGORY_ID) { - $categoryFilter = $this->filterBuilder - ->setField(CategoryProductLink::KEY_CATEGORY_ID) - ->setValue($filter->getValue()) - ->setConditionType($filter->getConditionType()) - ->create(); - - $this->filterGroupBuilder->addFilter($categoryFilter); - $categoryGroup = $this->filterGroupBuilder->create(); - $searchCriteriaForCollection->setFilterGroups([$categoryGroup]); - } - } - } - return $searchCriteriaForCollection; - } -} diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Search.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Search.php index 2c612da4f433..1038beddbecb 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Search.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Search.php @@ -13,13 +13,11 @@ use Magento\CatalogGraphQl\Model\Resolver\Products\SearchResult; use Magento\CatalogGraphQl\Model\Resolver\Products\SearchResultFactory; use Magento\Framework\Api\Search\SearchCriteriaInterface; -use Magento\Framework\App\ObjectManager; use Magento\Framework\GraphQl\Exception\GraphQlInputException; use Magento\Framework\GraphQl\Query\Resolver\ArgumentsProcessorInterface; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\GraphQl\Model\Query\ContextInterface; use Magento\Search\Api\SearchInterface; -use Magento\Search\Model\Search\PageSizeProvider; /** * Full text search for catalog using given search criteria. @@ -38,11 +36,6 @@ class Search implements ProductQueryInterface */ private $searchResultFactory; - /** - * @var PageSizeProvider - */ - private $pageSizeProvider; - /** * @var FieldSelection */ @@ -76,36 +69,31 @@ class Search implements ProductQueryInterface /** * @param SearchInterface $search * @param SearchResultFactory $searchResultFactory - * @param PageSizeProvider $pageSize * @param FieldSelection $fieldSelection * @param ProductSearch $productsProvider * @param SearchCriteriaBuilder $searchCriteriaBuilder - * @param ArgumentsProcessorInterface|null $argsSelection - * @param Suggestions|null $suggestions - * @param QueryPopularity|null $queryPopularity + * @param ArgumentsProcessorInterface $argsSelection + * @param Suggestions $suggestions + * @param QueryPopularity $queryPopularity */ public function __construct( SearchInterface $search, SearchResultFactory $searchResultFactory, - PageSizeProvider $pageSize, FieldSelection $fieldSelection, ProductSearch $productsProvider, SearchCriteriaBuilder $searchCriteriaBuilder, - ArgumentsProcessorInterface $argsSelection = null, - Suggestions $suggestions = null, - QueryPopularity $queryPopularity = null + ArgumentsProcessorInterface $argsSelection, + Suggestions $suggestions, + QueryPopularity $queryPopularity ) { $this->search = $search; $this->searchResultFactory = $searchResultFactory; - $this->pageSizeProvider = $pageSize; $this->fieldSelection = $fieldSelection; $this->productsProvider = $productsProvider; $this->searchCriteriaBuilder = $searchCriteriaBuilder; - $this->argsSelection = $argsSelection ?: ObjectManager::getInstance() - ->get(ArgumentsProcessorInterface::class); - $this->suggestions = $suggestions ?: ObjectManager::getInstance() - ->get(Suggestions::class); - $this->queryPopularity = $queryPopularity ?: ObjectManager::getInstance()->get(QueryPopularity::class); + $this->argsSelection = $argsSelection; + $this->suggestions = $suggestions; + $this->queryPopularity = $queryPopularity; } /** @@ -123,18 +111,7 @@ public function getResult( ContextInterface $context ): SearchResult { $searchCriteria = $this->buildSearchCriteria($args, $info); - - $realPageSize = $searchCriteria->getPageSize(); - $realCurrentPage = $searchCriteria->getCurrentPage(); - //Because of limitations of sort and pagination on search API we will query all IDS - $pageSize = $this->pageSizeProvider->getMaxPageSize(); - $searchCriteria->setPageSize($pageSize); - $searchCriteria->setCurrentPage(0); $itemsResults = $this->search->search($searchCriteria); - - //Address limitations of sort and pagination on search API apply original pagination from GQL query - $searchCriteria->setPageSize($realPageSize); - $searchCriteria->setCurrentPage($realCurrentPage); $searchResults = $this->productsProvider->getList( $searchCriteria, $itemsResults, @@ -142,7 +119,9 @@ public function getResult( $context ); - $totalPages = $realPageSize ? ((int)ceil($searchResults->getTotalCount() / $realPageSize)) : 0; + $totalPages = $searchCriteria->getPageSize() + ? ((int)ceil($searchResults->getTotalCount() / $searchCriteria->getPageSize())) + : 0; // add query statistics data if (!empty($args['search'])) { @@ -167,8 +146,8 @@ public function getResult( 'totalCount' => $totalCount, 'productsSearchResult' => $productArray, 'searchAggregation' => $itemsResults->getAggregations(), - 'pageSize' => $realPageSize, - 'currentPage' => $realCurrentPage, + 'pageSize' => $searchCriteria->getPageSize(), + 'currentPage' => $searchCriteria->getCurrentPage() + 1, //search criteria pagination starts with 0 'totalPages' => $totalPages, 'suggestions' => $suggestions, ] diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/SearchCriteria/CollectionProcessor/FilterProcessor/CategoryFilter.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/SearchCriteria/CollectionProcessor/FilterProcessor/CategoryFilter.php index d15c072b9fe4..e8825b95a9c7 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/SearchCriteria/CollectionProcessor/FilterProcessor/CategoryFilter.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/SearchCriteria/CollectionProcessor/FilterProcessor/CategoryFilter.php @@ -105,14 +105,11 @@ public function apply(Filter $filter, AbstractDb $collection) } elseif ($conditionType === self::CONDITION_TYPE_IN) { $this->joinMinimalPosition->execute($collection, $ids); } - /** Prevent filtering duplication as the filter should be already applied to the search result */ - if (!$collection->getFlag('search_resut_applied')) { - $collection->addCategoriesFilter( - [ - $conditionType => array_map('intval', $this->getCategoryIds($ids)) - ] - ); - } + $collection->addCategoriesFilter( + [ + $conditionType => array_map('intval', $this->getCategoryIds($ids)) + ] + ); } return true; diff --git a/app/code/Magento/CatalogGraphQl/Test/Unit/DataProvider/Product/SearchCriteriaBuilderTest.php b/app/code/Magento/CatalogGraphQl/Test/Unit/DataProvider/Product/SearchCriteriaBuilderTest.php index 59970335d3d1..95d631cfb171 100644 --- a/app/code/Magento/CatalogGraphQl/Test/Unit/DataProvider/Product/SearchCriteriaBuilderTest.php +++ b/app/code/Magento/CatalogGraphQl/Test/Unit/DataProvider/Product/SearchCriteriaBuilderTest.php @@ -117,7 +117,7 @@ public function testBuild(): void $this->sortOrderBuilder->expects($this->once()) ->method('setField') - ->with('_id') + ->with('entity_id') ->willReturnSelf(); $this->sortOrderBuilder->expects($this->once()) ->method('setDirection') diff --git a/app/code/Magento/CatalogGraphQl/Test/Unit/Model/Resolver/Products/Query/SearchTest.php b/app/code/Magento/CatalogGraphQl/Test/Unit/Model/Resolver/Products/Query/SearchTest.php index 8efd1914869d..2699de9b5a3b 100644 --- a/app/code/Magento/CatalogGraphQl/Test/Unit/Model/Resolver/Products/Query/SearchTest.php +++ b/app/code/Magento/CatalogGraphQl/Test/Unit/Model/Resolver/Products/Query/SearchTest.php @@ -18,10 +18,8 @@ use Magento\Framework\Api\Search\SearchResultInterface; use Magento\Framework\GraphQl\Query\Resolver\ArgumentsProcessorInterface; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; -use Magento\GraphQl\Model\Query\ContextExtensionInterface; use Magento\GraphQl\Model\Query\ContextInterface; use Magento\Search\Api\SearchInterface; -use Magento\Search\Model\Search\PageSizeProvider; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -42,11 +40,6 @@ class SearchTest extends TestCase */ private $searchResultFactory; - /** - * @var PageSizeProvider|MockObject - */ - private $pageSizeProvider; - /** * @var FieldSelection|MockObject */ @@ -94,9 +87,6 @@ protected function setUp(): void $this->searchResultFactory = $this->getMockBuilder(SearchResultFactory::class) ->disableOriginalConstructor() ->getMock(); - $this->pageSizeProvider = $this->getMockBuilder(PageSizeProvider::class) - ->disableOriginalConstructor() - ->getMock(); $this->fieldSelection = $this->getMockBuilder(FieldSelection::class) ->disableOriginalConstructor() ->getMock(); @@ -118,7 +108,6 @@ protected function setUp(): void $this->model = new Search( $this->search, $this->searchResultFactory, - $this->pageSizeProvider, $this->fieldSelection, $this->productsProvider, $this->searchCriteriaBuilder, diff --git a/app/code/Magento/CatalogGraphQl/Test/Unit/Model/Resolver/Products/SearchCriteria/CollectionProcessor/FilterProcessor/CategoryFilterTest.php b/app/code/Magento/CatalogGraphQl/Test/Unit/Model/Resolver/Products/SearchCriteria/CollectionProcessor/FilterProcessor/CategoryFilterTest.php index ab00dcb55157..7c865529bbd7 100644 --- a/app/code/Magento/CatalogGraphQl/Test/Unit/Model/Resolver/Products/SearchCriteria/CollectionProcessor/FilterProcessor/CategoryFilterTest.php +++ b/app/code/Magento/CatalogGraphQl/Test/Unit/Model/Resolver/Products/SearchCriteria/CollectionProcessor/FilterProcessor/CategoryFilterTest.php @@ -57,60 +57,6 @@ protected function setUp(): void ); } - /** - * Test that category filter works correctly with condition type "eq" - */ - public function testApplyWithConditionTypeEq(): void - { - $filter = new Filter(); - $category = $this->createMock(\Magento\Catalog\Model\Category::class); - $collection = $this->createMock(Collection::class); - $filter->setConditionType('eq'); - $categoryId = 1; - $filter->setValue($categoryId); - $this->categoryFactory->expects($this->once()) - ->method('create') - ->willReturn($category); - $this->categoryResourceModel->expects($this->once()) - ->method('load') - ->with($category, $categoryId); - $collection->expects($this->once()) - ->method('addCategoryFilter') - ->with($category); - $collection->expects($this->once()) - ->method('getFlag') - ->with('search_resut_applied') - ->willReturn(true); - $this->model->apply($filter, $collection); - } - - /** - * Test that category filter works correctly with condition type "in" and single category - */ - public function testApplyWithConditionTypeInAndSingleCategory(): void - { - $filter = new Filter(); - $category = $this->createMock(\Magento\Catalog\Model\Category::class); - $collection = $this->createMock(Collection::class); - $filter->setConditionType('in'); - $categoryId = 1; - $filter->setValue($categoryId); - $this->categoryFactory->expects($this->once()) - ->method('create') - ->willReturn($category); - $this->categoryResourceModel->expects($this->once()) - ->method('load') - ->with($category, $categoryId); - $collection->expects($this->once()) - ->method('addCategoryFilter') - ->with($category); - $collection->expects($this->once()) - ->method('getFlag') - ->with('search_resut_applied') - ->willReturn(true); - $this->model->apply($filter, $collection); - } - /** * Test that category filter works correctly with condition type "in" and multiple categories */ @@ -144,10 +90,6 @@ public function testApplyWithConditionTypeInAndMultipleCategories(): void $collection->expects($this->once()) ->method('addCategoriesFilter') ->with(['in' => [1, 2, 3]]); - $collection->expects($this->once()) - ->method('getFlag') - ->with('search_resut_applied') - ->willReturn(false); $category1->expects($this->once()) ->method('getIsAnchor') ->willReturn(true); @@ -191,10 +133,6 @@ public function testApplyWithOtherSupportedConditionTypes(string $condition): vo $collection->expects($this->once()) ->method('addCategoriesFilter') ->with([$condition => [1, 2]]); - $collection->expects($this->once()) - ->method('getFlag') - ->with('search_resut_applied') - ->willReturn(false); $category->expects($this->once()) ->method('getIsAnchor') ->willReturn(true); diff --git a/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection.php b/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection.php index 9b66606d37a9..8e9e3d19b176 100644 --- a/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection.php +++ b/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection.php @@ -223,6 +223,7 @@ public function __construct( * Get search. * * @deprecated 100.1.0 + * @see Should not be used anymore. * @return \Magento\Search\Api\SearchInterface */ private function getSearch() @@ -237,6 +238,7 @@ private function getSearch() * Test search. * * @deprecated 100.1.0 + * @see Should not be used anymore. * @param \Magento\Search\Api\SearchInterface $object * @return void * @since 100.1.0 @@ -250,6 +252,7 @@ public function setSearch(\Magento\Search\Api\SearchInterface $object) * Set search criteria builder. * * @deprecated 100.1.0 + * @see Should not be used anymore. * @return \Magento\Framework\Api\Search\SearchCriteriaBuilder */ private function getSearchCriteriaBuilder() @@ -265,6 +268,7 @@ private function getSearchCriteriaBuilder() * Set search criteria builder. * * @deprecated 100.1.0 + * @see Should not be used anymore. * @param \Magento\Framework\Api\Search\SearchCriteriaBuilder $object * @return void * @since 100.1.0 @@ -278,6 +282,7 @@ public function setSearchCriteriaBuilder(\Magento\Framework\Api\Search\SearchCri * Get filter builder. * * @deprecated 100.1.0 + * @see Should not be used anymore. * @return \Magento\Framework\Api\FilterBuilder */ private function getFilterBuilder() @@ -292,6 +297,7 @@ private function getFilterBuilder() * Set filter builder. * * @deprecated 100.1.0 + * @see Should not be used anymore. * @param \Magento\Framework\Api\FilterBuilder $object * @return void * @since 100.1.0 @@ -567,7 +573,7 @@ protected function _beforeLoad() * for the same requests and products with the same relevance * NOTE: this does not replace existing orders but ADDs one more */ - $this->setOrder('entity_id'); + $this->setOrder('entity_id', Select::SQL_ASC); return parent::_beforeLoad(); } diff --git a/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection/SearchResultApplier.php b/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection/SearchResultApplier.php index 36e0a85fa430..49676b7c3b20 100644 --- a/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection/SearchResultApplier.php +++ b/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection/SearchResultApplier.php @@ -27,24 +27,16 @@ class SearchResultApplier implements SearchResultApplierInterface */ private $searchResult; - /** - * @var array - */ - private $orders; - /** * @param Collection $collection * @param SearchResultInterface $searchResult - * @param array $orders */ public function __construct( Collection $collection, - SearchResultInterface $searchResult, - array $orders + SearchResultInterface $searchResult ) { $this->collection = $collection; $this->searchResult = $searchResult; - $this->orders = $orders; } /** @@ -56,18 +48,16 @@ public function apply() $this->collection->getSelect()->where('NULL'); return; } + $ids = []; foreach ($this->searchResult->getItems() as $item) { $ids[] = (int)$item->getId(); } $orderList = implode(',', $ids); - $this->collection->getSelect()->where('e.entity_id IN (?)', $ids); - - if (isset($this->orders['relevance'])) { - $this->collection->getSelect() - ->reset(\Magento\Framework\DB\Select::ORDER) - ->order(new \Magento\Framework\DB\Sql\Expression("FIELD(e.entity_id, $orderList)")); - } + $this->collection->getSelect() + ->where('e.entity_id IN (?)', $ids) + ->reset(\Magento\Framework\DB\Select::ORDER) + ->order(new \Magento\Framework\DB\Sql\Expression("FIELD(e.entity_id, $orderList)")); } } diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/BatchDataMapper/ProductDataMapper.php b/app/code/Magento/Elasticsearch/Model/Adapter/BatchDataMapper/ProductDataMapper.php index 0edc63b10f9a..2fe2a15bf059 100644 --- a/app/code/Magento/Elasticsearch/Model/Adapter/BatchDataMapper/ProductDataMapper.php +++ b/app/code/Magento/Elasticsearch/Model/Adapter/BatchDataMapper/ProductDataMapper.php @@ -311,7 +311,7 @@ function (string $valueId) { && in_array($attribute->getAttributeCode(), $this->sortableAttributesValuesToImplode) && count($attributeValues) > 1 ) { - $attributeValues = [$productId => implode(' ', $attributeValues)]; + $attributeValues = [$productId => implode("\n", $attributeValues)]; } if (in_array($attribute->getAttributeCode(), $this->sortableCaseSensitiveAttributes)) { diff --git a/app/code/Magento/Elasticsearch/SearchAdapter/Query/Builder/Sort.php b/app/code/Magento/Elasticsearch/SearchAdapter/Query/Builder/Sort.php index 7d41d54fb22a..9721c6652208 100644 --- a/app/code/Magento/Elasticsearch/SearchAdapter/Query/Builder/Sort.php +++ b/app/code/Magento/Elasticsearch/SearchAdapter/Query/Builder/Sort.php @@ -7,9 +7,7 @@ namespace Magento\Elasticsearch\SearchAdapter\Query\Builder; use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\AttributeProvider; -use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\ResolverInterface - as FieldNameResolver; -use Magento\Elasticsearch\Model\Adapter\FieldMapperInterface; +use Magento\Elasticsearch\SearchAdapter\Query\Builder\Sort\ExpressionBuilderInterface as SortExpressionBuilder; use Magento\Framework\Search\RequestInterface; /** @@ -17,56 +15,26 @@ */ class Sort { - /** - * List of fields that need to skipp by default. - */ - private const DEFAULT_SKIPPED_FIELDS = [ - 'entity_id', - ]; - - /** - * Default mapping for special fields. - */ - private const DEFAULT_MAP = [ - 'relevance' => '_score', - ]; - /** * @var AttributeProvider */ private $attributeAdapterProvider; /** - * @var FieldNameResolver - */ - private $fieldNameResolver; - - /** - * @var array + * @var SortExpressionBuilder */ - private $skippedFields; - - /** - * @var array - */ - private $map; + private $sortExpressionBuilder; /** * @param AttributeProvider $attributeAdapterProvider - * @param FieldNameResolver $fieldNameResolver - * @param array $skippedFields - * @param array $map + * @param SortExpressionBuilder $sortExpressionBuilder */ public function __construct( AttributeProvider $attributeAdapterProvider, - FieldNameResolver $fieldNameResolver, - array $skippedFields = [], - array $map = [] + SortExpressionBuilder $sortExpressionBuilder ) { $this->attributeAdapterProvider = $attributeAdapterProvider; - $this->fieldNameResolver = $fieldNameResolver; - $this->skippedFields = array_merge(self::DEFAULT_SKIPPED_FIELDS, $skippedFields); - $this->map = array_merge(self::DEFAULT_MAP, $map); + $this->sortExpressionBuilder = $sortExpressionBuilder; } /** @@ -74,9 +42,6 @@ public function __construct( * * @param RequestInterface $request * @return array - * - * @SuppressWarnings(PHPMD.CyclomaticComplexity) - * @SuppressWarnings(PHPMD.NPathComplexity) */ public function getSort(RequestInterface $request) { @@ -88,38 +53,11 @@ public function getSort(RequestInterface $request) if (!method_exists($request, 'getSort')) { return $sorts; } + foreach ($request->getSort() as $item) { - if (in_array($item['field'], $this->skippedFields)) { - continue; - } $attribute = $this->attributeAdapterProvider->getByAttributeCode((string)$item['field']); - $fieldName = $this->fieldNameResolver->getFieldName($attribute); - if (isset($this->map[$fieldName])) { - $fieldName = $this->map[$fieldName]; - } - if ($attribute->isSortable() && - !$attribute->isComplexType() && - !($attribute->isFloatType() || $attribute->isIntegerType()) - ) { - $suffix = $this->fieldNameResolver->getFieldName( - $attribute, - ['type' => FieldMapperInterface::TYPE_SORT] - ); - $fieldName .= '.' . $suffix; - } - if ($attribute->isComplexType() && $attribute->isSortable()) { - $fieldName .= '_value'; - $suffix = $this->fieldNameResolver->getFieldName( - $attribute, - ['type' => FieldMapperInterface::TYPE_SORT] - ); - $fieldName .= '.' . $suffix; - } - $sorts[] = [ - $fieldName => [ - 'order' => strtolower($item['direction'] ?? '') - ] - ]; + $direction = strtolower($item['direction'] ?? ''); + $sorts[] = $this->sortExpressionBuilder->build($attribute, $direction, $request); } return $sorts; diff --git a/app/code/Magento/Elasticsearch/SearchAdapter/Query/Builder/Sort/DefaultExpression.php b/app/code/Magento/Elasticsearch/SearchAdapter/Query/Builder/Sort/DefaultExpression.php new file mode 100644 index 000000000000..f7d19a02d23a --- /dev/null +++ b/app/code/Magento/Elasticsearch/SearchAdapter/Query/Builder/Sort/DefaultExpression.php @@ -0,0 +1,60 @@ +fieldNameResolver = $fieldNameResolver; + } + + /** + * @inheritdoc + */ + public function build(AttributeAdapter $attribute, string $direction, RequestInterface $request): array + { + $fieldName = $this->fieldNameResolver->getFieldName($attribute); + if ($attribute->isSortable() && + !$attribute->isComplexType() && + !($attribute->isFloatType() || $attribute->isIntegerType()) + ) { + $suffix = $this->fieldNameResolver->getFieldName( + $attribute, + ['type' => FieldMapperInterface::TYPE_SORT] + ); + $fieldName .= '.' . $suffix; + } + if ($attribute->isComplexType() && $attribute->isSortable()) { + $fieldName .= '_value'; + $suffix = $this->fieldNameResolver->getFieldName( + $attribute, + ['type' => FieldMapperInterface::TYPE_SORT] + ); + $fieldName .= '.' . $suffix; + } + + return [ + $fieldName => ['order' => $direction], + ]; + } +} diff --git a/app/code/Magento/Elasticsearch/SearchAdapter/Query/Builder/Sort/EntityId.php b/app/code/Magento/Elasticsearch/SearchAdapter/Query/Builder/Sort/EntityId.php new file mode 100644 index 000000000000..f0a3b0854770 --- /dev/null +++ b/app/code/Magento/Elasticsearch/SearchAdapter/Query/Builder/Sort/EntityId.php @@ -0,0 +1,31 @@ + [ + 'type' => 'number', + 'script' => [ + 'lang' => 'painless', + 'source' => 'Long.parseLong(doc[\'_id\'].value)', + ], + 'order' => $direction, + ], + ]; + } +} diff --git a/app/code/Magento/Elasticsearch/SearchAdapter/Query/Builder/Sort/ExpressionBuilder.php b/app/code/Magento/Elasticsearch/SearchAdapter/Query/Builder/Sort/ExpressionBuilder.php new file mode 100644 index 000000000000..f9f1c8a965cd --- /dev/null +++ b/app/code/Magento/Elasticsearch/SearchAdapter/Query/Builder/Sort/ExpressionBuilder.php @@ -0,0 +1,46 @@ +defaultExpressionBuilder = $defaultExpressionBuilder; + $this->customExpressionBuilders = $customExpressionBuilders; + } + + /** + * @inheritdoc + */ + public function build(AttributeAdapter $attribute, string $direction, RequestInterface $request): array + { + return isset($this->customExpressionBuilders[$attribute->getAttributeCode()]) + ? $this->customExpressionBuilders[$attribute->getAttributeCode()]->build($attribute, $direction, $request) + : $this->defaultExpressionBuilder->build($attribute, $direction, $request); + } +} diff --git a/app/code/Magento/Elasticsearch/SearchAdapter/Query/Builder/Sort/ExpressionBuilderInterface.php b/app/code/Magento/Elasticsearch/SearchAdapter/Query/Builder/Sort/ExpressionBuilderInterface.php new file mode 100644 index 000000000000..50314b5dcd76 --- /dev/null +++ b/app/code/Magento/Elasticsearch/SearchAdapter/Query/Builder/Sort/ExpressionBuilderInterface.php @@ -0,0 +1,24 @@ +fieldNameResolver = $fieldNameResolver; + } + + /** + * @inheritdoc + */ + public function build(AttributeAdapter $attribute, string $direction, RequestInterface $request): array + { + $sortParams = ['order' => $direction]; + + $categoryIds = $this->getCategoryIdsFromQuery($request->getQuery()); + if (count($categoryIds) > 1) { + $fieldNames = []; + foreach ($categoryIds as $categoryId) { + $fieldNames[] = $this->fieldNameResolver->getFieldName($attribute, ['categoryId' => $categoryId]); + } + $fieldName = '_script'; + $sortParams += [ + 'type' => 'number', + 'script' => [ + 'lang' => 'painless', + 'source' => <<