Skip to content

Commit 1a8e64c

Browse files
committed
MAGETWO-58964: Sort order doesn't work on the search result page
- Clean Search Requests cache when search weight of an attribute is changed - Make a search relevance calculation algorithm configurable for MySQL search adapter - Add documentation for private method Mapper::createAroundSelect - Fix typo in fixture for Catalog - Add integration test to ensure that search weight and search query boosting are working properly. - Add functional test to cover search relevance customization functionality
1 parent 06dd04b commit 1a8e64c

File tree

26 files changed

+915
-130
lines changed

26 files changed

+915
-130
lines changed

app/code/Magento/Catalog/Block/Product/ListProduct.php

Lines changed: 121 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@
77
namespace Magento\Catalog\Block\Product;
88

99
use Magento\Catalog\Api\CategoryRepositoryInterface;
10+
use Magento\Catalog\Block\Product\ProductList\Toolbar;
1011
use Magento\Catalog\Model\Category;
1112
use Magento\Catalog\Model\Product;
13+
use Magento\Catalog\Model\ResourceModel\Product\Collection;
1214
use Magento\Eav\Model\Entity\Collection\AbstractCollection;
1315
use Magento\Framework\Exception\NoSuchEntityException;
1416
use Magento\Framework\DataObject\IdentityInterface;
@@ -24,7 +26,7 @@ class ListProduct extends AbstractProduct implements IdentityInterface
2426
*
2527
* @var string
2628
*/
27-
protected $_defaultToolbarBlock = \Magento\Catalog\Block\Product\ProductList\Toolbar::class;
29+
protected $_defaultToolbarBlock = Toolbar::class;
2830

2931
/**
3032
* Product Collection
@@ -84,50 +86,23 @@ public function __construct(
8486
/**
8587
* Retrieve loaded category collection
8688
*
89+
* The goal of this method is to choose whether the existing collection should be returned
90+
* or a new one should be initialized.
91+
*
92+
* It is not just a caching logic, but also is a real logical check
93+
* because there are two ways how collection may be stored inside the block:
94+
* - Product collection may be passed externally by 'setCollection' method
95+
* - Product collection may be requested internally from the current Catalog Layer.
96+
*
97+
* And this method will return collection anyway,
98+
* even when it did not pass externally and therefore isn't cached yet
99+
*
87100
* @return AbstractCollection
88101
*/
89102
protected function _getProductCollection()
90103
{
91104
if ($this->_productCollection === null) {
92-
$layer = $this->getLayer();
93-
/* @var $layer \Magento\Catalog\Model\Layer */
94-
if ($this->getShowRootCategory()) {
95-
$this->setCategoryId($this->_storeManager->getStore()->getRootCategoryId());
96-
}
97-
98-
// if this is a product view page
99-
if ($this->_coreRegistry->registry('product')) {
100-
// get collection of categories this product is associated with
101-
$categories = $this->_coreRegistry->registry('product')
102-
->getCategoryCollection()->setPage(1, 1)
103-
->load();
104-
// if the product is associated with any category
105-
if ($categories->count()) {
106-
// show products from this category
107-
$this->setCategoryId(current($categories->getIterator()));
108-
}
109-
}
110-
111-
$origCategory = null;
112-
if ($this->getCategoryId()) {
113-
try {
114-
$category = $this->categoryRepository->get($this->getCategoryId());
115-
} catch (NoSuchEntityException $e) {
116-
$category = null;
117-
}
118-
119-
if ($category) {
120-
$origCategory = $layer->getCurrentCategory();
121-
$layer->setCurrentCategory($category);
122-
}
123-
}
124-
$this->_productCollection = $layer->getProductCollection();
125-
126-
$this->prepareSortableFieldsByCategory($layer->getCurrentCategory());
127-
128-
if ($origCategory) {
129-
$layer->setCurrentCategory($origCategory);
130-
}
105+
$this->_productCollection = $this->initializeProductCollection();
131106
}
132107

133108
return $this->_productCollection;
@@ -170,47 +145,17 @@ public function getMode()
170145
*/
171146
protected function _beforeToHtml()
172147
{
173-
$toolbar = $this->getToolbarBlock();
174-
175-
// called prepare sortable parameters
176148
$collection = $this->_getProductCollection();
177-
178-
// use sortable parameters
179-
$orders = $this->getAvailableOrders();
180-
if ($orders) {
181-
$toolbar->setAvailableOrders($orders);
182-
}
183-
$sort = $this->getSortBy();
184-
if ($sort) {
185-
$toolbar->setDefaultOrder($sort);
186-
}
187-
$dir = $this->getDefaultDirection();
188-
if ($dir) {
189-
$toolbar->setDefaultDirection($dir);
190-
}
191-
$modes = $this->getModes();
192-
if ($modes) {
193-
$toolbar->setModes($modes);
194-
}
195-
196-
// set collection to toolbar and apply sort
197-
$toolbar->setCollection($collection);
198-
199-
$this->setChild('toolbar', $toolbar);
200-
$this->_eventManager->dispatch(
201-
'catalog_block_product_list_collection',
202-
['collection' => $this->_getProductCollection()]
203-
);
204-
205-
$this->_getProductCollection()->load();
149+
$this->configureToolbar($this->getToolbarBlock(), $collection);
150+
$collection->load();
206151

207152
return parent::_beforeToHtml();
208153
}
209154

210155
/**
211156
* Retrieve Toolbar block
212157
*
213-
* @return \Magento\Catalog\Block\Product\ProductList\Toolbar
158+
* @return Toolbar
214159
*/
215160
public function getToolbarBlock()
216161
{
@@ -379,4 +324,107 @@ protected function getPriceRender()
379324
{
380325
return $this->getLayout()->getBlock('product.price.render.default');
381326
}
327+
328+
/**
329+
* Configures product collection from a layer and returns its instance.
330+
*
331+
* Also in the scope of a product collection configuration, this method initiates configuration of Toolbar.
332+
* The reason to do this is because we have a bunch of legacy code
333+
* where Toolbar configures several options of a collection and therefore this block depends on the Toolbar.
334+
*
335+
* This dependency leads to a situation where Toolbar sometimes called to configure a product collection,
336+
* and sometimes not.
337+
*
338+
* To unify this behavior and prevent potential bugs this dependency is explicitly called
339+
* when product collection initialized.
340+
*
341+
* @return Collection
342+
*/
343+
private function initializeProductCollection()
344+
{
345+
$layer = $this->getLayer();
346+
/* @var $layer \Magento\Catalog\Model\Layer */
347+
if ($this->getShowRootCategory()) {
348+
$this->setCategoryId($this->_storeManager->getStore()->getRootCategoryId());
349+
}
350+
351+
// if this is a product view page
352+
if ($this->_coreRegistry->registry('product')) {
353+
// get collection of categories this product is associated with
354+
$categories = $this->_coreRegistry->registry('product')
355+
->getCategoryCollection()->setPage(1, 1)
356+
->load();
357+
// if the product is associated with any category
358+
if ($categories->count()) {
359+
// show products from this category
360+
$this->setCategoryId(current($categories->getIterator()));
361+
}
362+
}
363+
364+
$origCategory = null;
365+
if ($this->getCategoryId()) {
366+
try {
367+
$category = $this->categoryRepository->get($this->getCategoryId());
368+
} catch (NoSuchEntityException $e) {
369+
$category = null;
370+
}
371+
372+
if ($category) {
373+
$origCategory = $layer->getCurrentCategory();
374+
$layer->setCurrentCategory($category);
375+
}
376+
}
377+
$collection = $layer->getProductCollection();
378+
379+
$this->prepareSortableFieldsByCategory($layer->getCurrentCategory());
380+
381+
if ($origCategory) {
382+
$layer->setCurrentCategory($origCategory);
383+
}
384+
385+
$toolbar = $this->getToolbarBlock();
386+
$this->configureToolbar($toolbar, $collection);
387+
388+
$this->_eventManager->dispatch(
389+
'catalog_block_product_list_collection',
390+
['collection' => $collection]
391+
);
392+
393+
return $collection;
394+
}
395+
396+
/**
397+
* Configures the Toolbar block with options from this block and configured product collection.
398+
*
399+
* The purpose of this method is the one-way sharing of different sorting related data
400+
* between this block, which is responsible for product list rendering,
401+
* and the Toolbar block, whose responsibility is a rendering of these options.
402+
*
403+
* @param ProductList\Toolbar $toolbar
404+
* @param Collection $collection
405+
* @return void
406+
*/
407+
private function configureToolbar(Toolbar $toolbar, Collection $collection)
408+
{
409+
// use sortable parameters
410+
$orders = $this->getAvailableOrders();
411+
if ($orders) {
412+
$toolbar->setAvailableOrders($orders);
413+
}
414+
$sort = $this->getSortBy();
415+
if ($sort) {
416+
$toolbar->setDefaultOrder($sort);
417+
}
418+
$dir = $this->getDefaultDirection();
419+
if ($dir) {
420+
$toolbar->setDefaultDirection($dir);
421+
}
422+
$modes = $this->getModes();
423+
if ($modes) {
424+
$toolbar->setModes($modes);
425+
}
426+
// set collection to toolbar and apply sort
427+
$toolbar->setCollection($collection);
428+
$this->setChild('toolbar', $toolbar);
429+
}
382430
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<?php
2+
/**
3+
* Copyright © 2013-2017 Magento. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
7+
namespace Magento\CatalogSearch\Model\Attribute;
8+
9+
/**
10+
* This plugin is responsible for processing of search_weight property of a product attribute,
11+
* which is used to boost matches by specific attributes.
12+
*
13+
* This is part of search accuracy customization functionality.
14+
*/
15+
class SearchWeight
16+
{
17+
/**
18+
* @param \Magento\Framework\Search\Request\Config $config
19+
*/
20+
public function __construct(
21+
\Magento\Framework\Search\Request\Config $config
22+
) {
23+
$this->config = $config;
24+
}
25+
26+
/**
27+
* Cleans a cache of search requests when attribute's search weight is changed.
28+
*
29+
* A product attribute in Magento contains a property named 'search_weight'.
30+
* This property should be passed to a search adapter.
31+
* And container which is responsible for this is the Search Request.
32+
*
33+
* However, search requests are dynamically generated and therefore cached in the Configuration cache.
34+
*
35+
* But, as they're cached, there is a problem when search weight is changed for an attribute
36+
* as it will not change in the cache.
37+
*
38+
* This plugin solves this issue by resetting cache of search requests
39+
* when an attribute's search weight is changed.
40+
*
41+
* @param \Magento\Catalog\Model\ResourceModel\Attribute $subject
42+
* @param \Closure $proceed
43+
* @param \Magento\Framework\Model\AbstractModel $attribute
44+
* @return \Magento\Catalog\Model\ResourceModel\Attribute
45+
*
46+
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
47+
*/
48+
public function aroundSave(
49+
\Magento\Catalog\Model\ResourceModel\Attribute $subject,
50+
\Closure $proceed,
51+
\Magento\Framework\Model\AbstractModel $attribute
52+
) {
53+
$isNew = $attribute->isObjectNew();
54+
$isWeightChanged = $attribute->dataHasChangedFor('search_weight');
55+
56+
$result = $proceed($attribute);
57+
if ($isNew || $isWeightChanged) {
58+
$this->config->reset();
59+
}
60+
61+
return $result;
62+
}
63+
}

app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Plugin/Attribute.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ public function __construct(
3737
\Magento\Framework\Indexer\IndexerRegistry $indexerRegistry,
3838
\Magento\Framework\Search\Request\Config $config
3939
) {
40-
parent::__construct($indexerRegistry); // TODO: Change the autogenerated stub
40+
parent::__construct($indexerRegistry);
4141
$this->config = $config;
4242
}
4343

0 commit comments

Comments
 (0)