diff --git a/app/code/Magento/Catalog/Block/Product/View/Attributes.php b/app/code/Magento/Catalog/Block/Product/View/Attributes.php index fbdda684343b5..32c1c1b6d7a61 100644 --- a/app/code/Magento/Catalog/Block/Product/View/Attributes.php +++ b/app/code/Magento/Catalog/Block/Product/View/Attributes.php @@ -81,8 +81,9 @@ public function getAdditionalData(array $excludeAttr = []) $attributes = $product->getAttributes(); foreach ($attributes as $attribute) { if ($attribute->getIsVisibleOnFront() && !in_array($attribute->getAttributeCode(), $excludeAttr)) { - $value = $attribute->getFrontend()->getValue($product); - + if (is_array($value = $attribute->getFrontend()->getValue($product))) { + continue; + } if (!$product->hasData($attribute->getAttributeCode())) { $value = __('N/A'); } elseif ((string)$value == '') { @@ -90,7 +91,6 @@ public function getAdditionalData(array $excludeAttr = []) } elseif ($attribute->getFrontendInput() == 'price' && is_string($value)) { $value = $this->priceCurrency->convertAndFormat($value); } - if ($value instanceof Phrase || (is_string($value) && strlen($value))) { $data[$attribute->getAttributeCode()] = [ 'label' => __($attribute->getStoreLabel()), diff --git a/app/code/Magento/Search/Model/SynonymAnalyzer.php b/app/code/Magento/Search/Model/SynonymAnalyzer.php index 6d5daf6115c0d..1c11e35aaf72d 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 { /** @@ -42,55 +47,119 @@ public function __construct(SynonymReader $synReader) */ 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[] + */ + private function getSynonymGroupsByPhrase(string $phrase): array + { + $result = []; + + $synonymGroups = $this->synReaderModel->loadByPhrase($phrase)->getData(); + foreach ($synonymGroups as $row) { + $result[] = $row['synonyms']; + } + return $result; } } 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();