diff --git a/app/code/Magento/CatalogInventory/Model/Indexer/Stock/AbstractAction.php b/app/code/Magento/CatalogInventory/Model/Indexer/Stock/AbstractAction.php
index 1e7c800ea1c38..85fee62eb4303 100644
--- a/app/code/Magento/CatalogInventory/Model/Indexer/Stock/AbstractAction.php
+++ b/app/code/Magento/CatalogInventory/Model/Indexer/Stock/AbstractAction.php
@@ -10,6 +10,7 @@
use Magento\Framework\App\ObjectManager;
use Magento\Framework\App\ResourceConnection;
+use Magento\Framework\EntityManager\MetadataPool;
/**
* Abstract action reindex class
@@ -70,25 +71,33 @@ abstract class AbstractAction
*/
private $cacheCleaner;
+ /**
+ * @var MetadataPool
+ */
+ private $metadataPool;
+
/**
* @param ResourceConnection $resource
* @param \Magento\CatalogInventory\Model\ResourceModel\Indexer\StockFactory $indexerFactory
* @param \Magento\Catalog\Model\Product\Type $catalogProductType
* @param \Magento\Framework\Indexer\CacheContext $cacheContext
* @param \Magento\Framework\Event\ManagerInterface $eventManager
+ * @param MetadataPool|null $metadataPool
*/
public function __construct(
ResourceConnection $resource,
\Magento\CatalogInventory\Model\ResourceModel\Indexer\StockFactory $indexerFactory,
\Magento\Catalog\Model\Product\Type $catalogProductType,
\Magento\Framework\Indexer\CacheContext $cacheContext,
- \Magento\Framework\Event\ManagerInterface $eventManager
+ \Magento\Framework\Event\ManagerInterface $eventManager,
+ MetadataPool $metadataPool = null
) {
$this->_resource = $resource;
$this->_indexerFactory = $indexerFactory;
$this->_catalogProductType = $catalogProductType;
$this->cacheContext = $cacheContext;
$this->eventManager = $eventManager;
+ $this->metadataPool = $metadataPool ?: ObjectManager::getInstance()->get(MetadataPool::class);
}
/**
@@ -154,10 +163,15 @@ protected function _getTable($entityName)
public function getRelationsByChild($childIds)
{
$connection = $this->_getConnection();
- $select = $connection->select()
- ->from($this->_getTable('catalog_product_relation'), 'parent_id')
- ->where('child_id IN(?)', $childIds);
-
+ $linkField = $this->metadataPool->getMetadata(\Magento\Catalog\Api\Data\ProductInterface::class)
+ ->getLinkField();
+ $select = $connection->select()->from(
+ ['cpe' => $this->_getTable('catalog_product_entity')],
+ 'entity_id'
+ )->join(
+ ['relation' => $this->_getTable('catalog_product_relation')],
+ 'relation.parent_id = cpe.' . $linkField
+ )->where('child_id IN(?)', $childIds);
return $connection->fetchCol($select);
}
@@ -230,7 +244,8 @@ protected function _reindexRows($productIds = [])
if (!is_array($productIds)) {
$productIds = [$productIds];
}
-
+ $parentIds = $this->getRelationsByChild($productIds);
+ $productIds = $parentIds ? array_unique(array_merge($parentIds, $productIds)) : $productIds;
$this->getCacheCleaner()->clean($productIds, function () use ($productIds) {
$this->doReindex($productIds);
});
@@ -248,13 +263,10 @@ private function doReindex($productIds = [])
{
$connection = $this->_getConnection();
- $parentIds = $this->getRelationsByChild($productIds);
- $processIds = $parentIds ? array_merge($parentIds, $productIds) : $productIds;
-
// retrieve product types by processIds
$select = $connection->select()
->from($this->_getTable('catalog_product_entity'), ['entity_id', 'type_id'])
- ->where('entity_id IN(?)', $processIds);
+ ->where('entity_id IN(?)', $productIds);
$pairs = $connection->fetchPairs($select);
$byType = [];
diff --git a/app/code/Magento/CatalogInventory/Model/Plugin/ReindexUpdatedProducts.php b/app/code/Magento/CatalogInventory/Model/Plugin/ReindexUpdatedProducts.php
new file mode 100644
index 0000000000000..f10afcd4ea329
--- /dev/null
+++ b/app/code/Magento/CatalogInventory/Model/Plugin/ReindexUpdatedProducts.php
@@ -0,0 +1,46 @@
+indexerProcessor = $indexerProcessor;
+ }
+
+ /**
+ * Reindex on product attribute mass change
+ *
+ * @param ProductAction $subject
+ * @param ProductAction $action
+ * @param array $productIds
+ * @return ProductAction
+ *
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ */
+ public function afterUpdateAttributes(
+ ProductAction $subject,
+ ProductAction $action,
+ $productIds
+ ) {
+ $this->indexerProcessor->reindexList(array_unique($productIds));
+ return $action;
+ }
+}
diff --git a/app/code/Magento/CatalogInventory/etc/di.xml b/app/code/Magento/CatalogInventory/etc/di.xml
index 2a55d745e1185..65bc277121429 100644
--- a/app/code/Magento/CatalogInventory/etc/di.xml
+++ b/app/code/Magento/CatalogInventory/etc/di.xml
@@ -78,6 +78,14 @@
+
+
+
+
+
+ Magento\CatalogInventory\Model\Indexer\Stock\Processor
+
+
diff --git a/app/code/Magento/Search/Model/SynonymAnalyzer.php b/app/code/Magento/Search/Model/SynonymAnalyzer.php
index 6d5daf6115c0d..a63c10da169d7 100644
--- a/app/code/Magento/Search/Model/SynonymAnalyzer.php
+++ b/app/code/Magento/Search/Model/SynonymAnalyzer.php
@@ -3,10 +3,15 @@
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
+
+declare(strict_types=1);
namespace Magento\Search\Model;
use Magento\Search\Api\SynonymAnalyzerInterface;
+/**
+ * SynonymAnalyzer responsible for search of synonyms matching a word or a phrase.
+ */
class SynonymAnalyzer implements SynonymAnalyzerInterface
{
/**
@@ -39,58 +44,125 @@ public function __construct(SynonymReader $synReader)
* ]
* @param string $phrase
* @return array
+ * @throws \Magento\Framework\Exception\LocalizedException
*/
public function getSynonymsForPhrase($phrase)
{
- $synGroups = [];
+ $result = [];
- if (empty($phrase)) {
- return $synGroups;
+ if (empty(trim($phrase))) {
+ return $result;
}
- $rows = $this->synReaderModel->loadByPhrase($phrase)->getData();
- $synonyms = [];
- foreach ($rows as $row) {
- $synonyms [] = $row['synonyms'];
- }
+ $synonymGroups = $this->getSynonymGroupsByPhrase($phrase);
+
+ // Replace multiple spaces in a row with the only one space
+ $phrase = preg_replace("/ {2,}/", " ", $phrase);
// Go through every returned record looking for presence of the actual phrase. If there were no matching
// records found in DB then create a new entry for it in the returned array
$words = explode(' ', $phrase);
- foreach ($words as $w) {
- $position = $this->findInArray($w, $synonyms);
- if ($position !== false) {
- $synGroups[] = explode(',', $synonyms[$position]);
- } else {
- // No synonyms were found. Return the original word in this position
- $synGroups[] = [$w];
+
+ foreach ($words as $offset => $word) {
+ $synonyms = [$word];
+
+ if ($synonymGroups) {
+ $pattern = $this->getSearchPattern(\array_slice($words, $offset));
+ $position = $this->findInArray($pattern, $synonymGroups);
+ if ($position !== null) {
+ $synonyms = explode(',', $synonymGroups[$position]);
+ }
}
+
+ $result[] = $synonyms;
}
- return $synGroups;
+
+ return $result;
}
/**
- * Helper method to find the presence of $word in $wordsArray. If found, the particular array index is returned.
+ * Helper method to find the matching of $pattern to $synonymGroupsToExamine.
+ * If matches, the particular array index is returned.
* Otherwise false will be returned.
*
- * @param string $word
- * @param $array $wordsArray
- * @return boolean | int
+ * @param string $pattern
+ * @param array $synonymGroupsToExamine
+ * @return int|null
*/
- private function findInArray($word, $wordsArray)
+ private function findInArray(string $pattern, array $synonymGroupsToExamine)
{
- if (empty($wordsArray)) {
- return false;
- }
$position = 0;
- foreach ($wordsArray as $wordsLine) {
- $pattern = '/^' . $word . ',|,' . $word . ',|,' . $word . '$/';
- $rv = preg_match($pattern, $wordsLine);
- if ($rv != 0) {
+ foreach ($synonymGroupsToExamine as $synonymGroup) {
+ $matchingResultCode = preg_match($pattern, $synonymGroup);
+ if ($matchingResultCode === 1) {
return $position;
}
$position++;
}
- return false;
+ return null;
+ }
+
+ /**
+ * Returns a regular expression to search for synonyms of the phrase represented as the list of words.
+ *
+ * Returned pattern contains expression to search for a part of the phrase from the beginning.
+ *
+ * For example, in the phrase "Elizabeth is the English queen" with subset from the very first word,
+ * the method will build an expression which looking for synonyms for all these patterns:
+ * - Elizabeth is the English queen
+ * - Elizabeth is the English
+ * - Elizabeth is the
+ * - Elizabeth is
+ * - Elizabeth
+ *
+ * For the same phrase on the second iteration with the first word "is" it will match for these synonyms:
+ * - is the English queen
+ * - is the English
+ * - is the
+ * - is
+ *
+ * The pattern looking for exact match and will not find these phrases as synonyms:
+ * - Is there anybody in the room?
+ * - Is the English is most popular language?
+ * - Is the English queen Elizabeth?
+ *
+ * Take into account that returned pattern expects that data will be represented as comma-separated value.
+ *
+ * @param array $words
+ * @return string
+ */
+ private function getSearchPattern(array $words): string
+ {
+ $patterns = [];
+ for ($lastItem = count($words); $lastItem > 0; $lastItem--) {
+ $phrase = implode("\s+", \array_slice($words, 0, $lastItem));
+ $patterns[] = '^' . $phrase . ',';
+ $patterns[] = ',' . $phrase . ',';
+ $patterns[] = ',' . $phrase . '$';
+ }
+
+ $pattern = '/' . implode('|', $patterns) . '/i';
+ return $pattern;
+ }
+
+ /**
+ * Get all synonym groups for the phrase
+ *
+ * Returns an array of synonyms which are represented as comma-separated value for each item in the list
+ *
+ * @param string $phrase
+ * @return string[]
+ * @throws \Magento\Framework\Exception\LocalizedException
+ */
+ private function getSynonymGroupsByPhrase(string $phrase): array
+ {
+ $result = [];
+
+ /** @var array $synonymGroups */
+ $synonymGroups = $this->synReaderModel->loadByPhrase($phrase)->getData();
+ foreach ($synonymGroups as $row) {
+ $result[] = $row['synonyms'];
+ }
+ return $result;
}
}
diff --git a/app/code/Magento/Search/Model/SynonymReader.php b/app/code/Magento/Search/Model/SynonymReader.php
index 202931665f493..078a3eb178cbe 100644
--- a/app/code/Magento/Search/Model/SynonymReader.php
+++ b/app/code/Magento/Search/Model/SynonymReader.php
@@ -78,6 +78,7 @@ protected function _construct()
*
* @param string $phrase
* @return $this
+ * @throws \Magento\Framework\Exception\LocalizedException
* @since 100.1.0
*/
public function loadByPhrase($phrase)
diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/ActionTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/ActionTest.php
index 90825ab573d09..39905aeae10f5 100644
--- a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/ActionTest.php
+++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/ActionTest.php
@@ -84,6 +84,73 @@ public function testUpdateWebsites()
}
}
+ /**
+ * @magentoDataFixture Magento/ConfigurableProduct/_files/product_configurable.php
+ * @magentoAppArea adminhtml
+ * @param string $status
+ * @param string $productsCount
+ * @dataProvider updateAttributesDataProvider
+ */
+ public function testUpdateAttributes($status, $productsCount)
+ {
+ /** @var \Magento\Framework\Indexer\IndexerRegistry $indexerRegistry */
+ $indexerRegistry = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()
+ ->get(\Magento\Framework\Indexer\IndexerRegistry::class);
+ $indexerRegistry->get(Fulltext::INDEXER_ID)->setScheduled(false);
+
+ /** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */
+ $productRepository = $this->objectManager->create(\Magento\Catalog\Api\ProductRepositoryInterface::class);
+
+ /** @var \Magento\Catalog\Model\Product $product */
+ $product = $productRepository->get('configurable');
+ $productAttributesOptions = $product->getExtensionAttributes()->getConfigurableProductLinks();
+ $attrData = ['status' => $status];
+ $configurableOptionsId = [];
+ if (isset($productAttributesOptions)) {
+ foreach ($productAttributesOptions as $configurableOption) {
+ $configurableOptionsId[] = $configurableOption;
+ }
+ }
+ $this->action->updateAttributes($configurableOptionsId, $attrData, $product->getStoreId());
+
+ $categoryFactory = $this->objectManager->create(\Magento\Catalog\Model\CategoryFactory::class);
+ /** @var \Magento\Catalog\Block\Product\ListProduct $listProduct */
+ $listProduct = $this->objectManager->create(\Magento\Catalog\Block\Product\ListProduct::class);
+ $category = $categoryFactory->create()->load(2);
+ $layer = $listProduct->getLayer();
+ $layer->setCurrentCategory($category);
+ $productCollection = $layer->getProductCollection();
+ $productCollection->joinField(
+ 'qty',
+ 'cataloginventory_stock_status',
+ 'qty',
+ 'product_id=entity_id',
+ '{{table}}.stock_id=1',
+ 'left'
+ );
+
+ $this->assertEquals($productsCount, $productCollection->count());
+ }
+
+ /**
+ * DataProvider for testUpdateAttributes
+ *
+ * @return array
+ */
+ public function updateAttributesDataProvider()
+ {
+ return [
+ [
+ 'status' => 2,
+ 'expected_count' => 0
+ ],
+ [
+ 'status' => 1,
+ 'expected_count' => 1
+ ],
+ ];
+ }
+
public static function tearDownAfterClass()
{
/** @var \Magento\Framework\Indexer\IndexerRegistry $indexerRegistry */
diff --git a/dev/tests/integration/testsuite/Magento/Search/Model/SynonymAnalyzerTest.php b/dev/tests/integration/testsuite/Magento/Search/Model/SynonymAnalyzerTest.php
index 892ab57080a98..9fc9b10c89fa4 100644
--- a/dev/tests/integration/testsuite/Magento/Search/Model/SynonymAnalyzerTest.php
+++ b/dev/tests/integration/testsuite/Magento/Search/Model/SynonymAnalyzerTest.php
@@ -42,10 +42,38 @@ public static function loadGetSynonymsForPhraseDataProvider()
'phrase' => 'universe is enormous',
'expectedResult' => [['universe', 'cosmos'], ['is'], ['big', 'huge', 'large', 'enormous']]
],
+ 'WithCaseMismatch' => [
+ 'phrase' => 'GNU\'s Not Unix',
+ 'expectedResult' => [['GNU\'s'], ['Not'], ['unix', 'linux'],]
+ ],
+ 'WithMultiWordPhrase' => [
+ 'phrase' => 'Coastline of Great Britain stretches for 11,073 miles',
+ 'expectedResult' => [
+ ['Coastline'],
+ ['of'],
+ ['Great Britain', 'United Kingdom'],
+ ['Britain'],
+ ['stretches'],
+ ['for'],
+ ['11,073'],
+ ['miles']
+ ]
+ ],
+ 'PartialSynonymMatching' => [
+ 'phrase' => 'Magento Engineering',
+ 'expectedResult' => [
+ ['orange', 'magento'],
+ ['Engineering', 'Technical Staff']
+ ]
+ ],
'noSynonyms' => [
'phrase' => 'this sentence has no synonyms',
'expectedResult' => [['this'], ['sentence'], ['has'], ['no'], ['synonyms']]
],
+ 'multipleSpaces' => [
+ 'phrase' => 'GNU\'s Not Unix',
+ 'expectedResult' => [['GNU\'s'], ['Not'], ['unix', 'linux'],]
+ ],
'oneMoreTest' => [
'phrase' => 'schlicht',
'expectedResult' => [['schlicht', 'natürlich']]
diff --git a/dev/tests/integration/testsuite/Magento/Search/_files/synonym_reader.php b/dev/tests/integration/testsuite/Magento/Search/_files/synonym_reader.php
index f529b61522967..78c30cf458c51 100644
--- a/dev/tests/integration/testsuite/Magento/Search/_files/synonym_reader.php
+++ b/dev/tests/integration/testsuite/Magento/Search/_files/synonym_reader.php
@@ -24,9 +24,22 @@
$synonymsModel = $objectManager->create(\Magento\Search\Model\SynonymReader::class);
$synonymsModel->setSynonyms('hill,mountain,peak')->setWebsiteId(1)->save();
+$synonymsModel = $objectManager->create(\Magento\Search\Model\SynonymReader::class);
+$synonymsModel->setSynonyms('Community Engineering,Contributors,Magento Community Engineering')->setWebsiteId(1)
+ ->save();
+
+$synonymsModel = $objectManager->create(\Magento\Search\Model\SynonymReader::class);
+$synonymsModel->setSynonyms('Engineering,Technical Staff')->setWebsiteId(1)->save();
+
// Synonym groups for "All Store Views"
$synonymsModel = $objectManager->create(\Magento\Search\Model\SynonymReader::class);
$synonymsModel->setSynonyms('universe,cosmos')->setWebsiteId(0)->save();
+$synonymsModel = $objectManager->create(\Magento\Search\Model\SynonymReader::class);
+$synonymsModel->setSynonyms('unix,linux')->setWebsiteId(0)->save();
+
+$synonymsModel = $objectManager->create(\Magento\Search\Model\SynonymReader::class);
+$synonymsModel->setSynonyms('Great Britain,United Kingdom')->setWebsiteId(0)->save();
+
$synonymsModel = $objectManager->create(\Magento\Search\Model\SynonymReader::class);
$synonymsModel->setSynonyms('big,huge,large,enormous')->setWebsiteId(0)->save();