Skip to content

Commit 5848057

Browse files
committed
MC-31304: [ElasticSearch] Exception on catalog search result page
1 parent 818ca3e commit 5848057

File tree

3 files changed

+147
-40
lines changed
  • app/code/Magento/Elasticsearch
  • dev/tests/integration/testsuite/Magento/Elasticsearch/Model/Indexer/Fulltext/Plugin/Category/Product

3 files changed

+147
-40
lines changed

app/code/Magento/Elasticsearch/Model/Indexer/Fulltext/Plugin/Category/Product/Attribute.php

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
use Magento\Elasticsearch\Model\Indexer\IndexerHandler as ElasticsearchIndexerHandler;
1414
use Magento\Framework\Indexer\DimensionProviderInterface;
1515
use Magento\CatalogSearch\Model\Indexer\IndexerHandlerFactory;
16+
use Magento\Framework\Model\AbstractModel;
1617
use Magento\Framework\Exception\LocalizedException;
1718

1819
/**
@@ -40,6 +41,11 @@ class Attribute
4041
*/
4142
private $indexerHandlerFactory;
4243

44+
/**
45+
* @var bool
46+
*/
47+
private $isNewObject;
48+
4349
/**
4450
* @param Config $config
4551
* @param Processor $indexerProcessor
@@ -72,7 +78,7 @@ public function afterSave(
7278
AttributeResourceModel $result
7379
): AttributeResourceModel {
7480
$indexer = $this->indexerProcessor->getIndexer();
75-
if ($indexer->isInvalid()
81+
if ($this->isNewObject
7682
&& !$indexer->isScheduled()
7783
&& $this->config->isElasticsearchEnabled()
7884
) {
@@ -89,4 +95,19 @@ public function afterSave(
8995

9096
return $result;
9197
}
98+
99+
/**
100+
* Check if mapping needs to be updated (attribute is new).
101+
*
102+
* @param AttributeResourceModel $subject
103+
* @param AbstractModel $attribute
104+
* @return void
105+
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
106+
*/
107+
public function beforeSave(
108+
AttributeResourceModel $subject,
109+
AbstractModel $attribute
110+
): void {
111+
$this->isNewObject = $attribute->isObjectNew();
112+
}
92113
}

app/code/Magento/Elasticsearch/Test/Unit/Model/Indexer/Fulltext/Plugin/Category/Product/AttributeTest.php

Lines changed: 25 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,17 @@
99

1010
use ArrayIterator;
1111
use Magento\Catalog\Model\ResourceModel\Attribute as AttributeResourceModel;
12+
use Magento\Catalog\Model\ResourceModel\Eav\Attribute as AttributeModel;
1213
use Magento\CatalogSearch\Model\Indexer\Fulltext\Processor;
1314
use Magento\CatalogSearch\Model\Indexer\IndexerHandlerFactory;
1415
use Magento\Elasticsearch\Model\Config;
15-
use Magento\Elasticsearch\Model\Indexer\Fulltext\Plugin\Category\Product\Attribute;
16+
use Magento\Elasticsearch\Model\Indexer\Fulltext\Plugin\Category\Product\Attribute as AttributePlugin;
1617
use Magento\Elasticsearch\Model\Indexer\IndexerHandler;
1718
use Magento\Framework\Indexer\DimensionProviderInterface;
1819
use Magento\Framework\Indexer\IndexerInterface;
1920
use Magento\Framework\TestFramework\Unit\Helper\ObjectManager;
20-
use PHPUnit\Framework\MockObject\Rule\InvokedCount as InvokedCountMatcher;
2121
use PHPUnit\Framework\MockObject\MockObject;
22+
use PHPUnit\Framework\MockObject\Rule\InvokedCount as InvokedCountMatcher;
2223
use PHPUnit\Framework\TestCase;
2324

2425
/**
@@ -47,7 +48,7 @@ class AttributeTest extends TestCase
4748
private $indexerHandlerFactoryMock;
4849

4950
/**
50-
* @var Attribute
51+
* @var AttributePlugin
5152
*/
5253
private $attributePlugin;
5354

@@ -65,7 +66,7 @@ protected function setUp(): void
6566
$this->indexerHandlerFactoryMock = $this->createMock(IndexerHandlerFactory::class);
6667

6768
$this->attributePlugin = (new ObjectManager($this))->getObject(
68-
Attribute::class,
69+
AttributePlugin::class,
6970
[
7071
'config' => $this->configMock,
7172
'indexerProcessor' => $this->indexerProcessorMock,
@@ -78,55 +79,56 @@ protected function setUp(): void
7879
/**
7980
* Test update catalog search indexer process.
8081
*
81-
* @param bool $isInvalid
82+
* @param bool $isNewObject
8283
* @param bool $isElasticsearchEnabled
8384
* @param array $dimensions
8485
* @return void
8586
* @dataProvider afterSaveDataProvider
8687
*
8788
*/
88-
public function testAfterSave(bool $isInvalid, bool $isElasticsearchEnabled, array $dimensions): void
89+
public function testAfterSave(bool $isNewObject, bool $isElasticsearchEnabled, array $dimensions): void
8990
{
90-
$indexerData = ['indexer_example_data'];
91-
91+
/** @var AttributeModel $attribute */
92+
$attribute = (new ObjectManager($this))->getObject(AttributeModel::class);
93+
$attribute->isObjectNew($isNewObject);
9294
/** @var AttributeResourceModel|MockObject $subjectMock */
9395
$subjectMock = $this->createMock(AttributeResourceModel::class);
96+
$this->attributePlugin->beforeSave($subjectMock, $attribute);
97+
98+
$indexerData = ['indexer_example_data'];
99+
94100
/** @var IndexerInterface|MockObject $indexerMock */
95101
$indexerMock = $this->getMockBuilder(IndexerInterface::class)
96-
->setMethods(['isInvalid', 'getData'])
102+
->setMethods(['getData'])
97103
->disableOriginalConstructor()
98104
->getMockForAbstractClass();
99105

100-
$indexerMock->expects($this->once())
101-
->method('isInvalid')
102-
->willReturn($isInvalid);
103-
104-
$indexerMock->expects($this->getExpectsCount($isInvalid, $isElasticsearchEnabled))
106+
$indexerMock->expects($this->getExpectsCount($isNewObject, $isElasticsearchEnabled))
105107
->method('getData')
106108
->willReturn($indexerData);
107109

108110
$this->indexerProcessorMock->expects($this->once())
109111
->method('getIndexer')
110112
->willReturn($indexerMock);
111113

112-
$this->configMock->expects($isInvalid ? $this->once() : $this->never())
114+
$this->configMock->expects($isNewObject ? $this->once() : $this->never())
113115
->method('isElasticsearchEnabled')
114116
->willReturn($isElasticsearchEnabled);
115117

116118
/** @var IndexerHandler|MockObject $indexerHandlerMock */
117119
$indexerHandlerMock = $this->createMock(IndexerHandler::class);
118120

119121
$indexerHandlerMock
120-
->expects(($isInvalid && $isElasticsearchEnabled) ? $this->exactly(count($dimensions)) : $this->never())
122+
->expects(($isNewObject && $isElasticsearchEnabled) ? $this->exactly(count($dimensions)) : $this->never())
121123
->method('updateIndex')
122124
->willReturnSelf();
123125

124-
$this->indexerHandlerFactoryMock->expects($this->getExpectsCount($isInvalid, $isElasticsearchEnabled))
126+
$this->indexerHandlerFactoryMock->expects($this->getExpectsCount($isNewObject, $isElasticsearchEnabled))
125127
->method('create')
126128
->with(['data' => $indexerData])
127129
->willReturn($indexerHandlerMock);
128130

129-
$this->dimensionProviderMock->expects($this->getExpectsCount($isInvalid, $isElasticsearchEnabled))
131+
$this->dimensionProviderMock->expects($this->getExpectsCount($isNewObject, $isElasticsearchEnabled))
130132
->method('getIterator')
131133
->willReturn(new ArrayIterator($dimensions));
132134

@@ -143,8 +145,8 @@ public function afterSaveDataProvider(): array
143145
$dimensions = [['scope' => 1], ['scope' => 2]];
144146

145147
return [
146-
'save_without_invalidation' => [false, false, []],
147-
'save_with_mysql_search' => [true, false, $dimensions],
148+
'save_existing_object' => [false, false, $dimensions],
149+
'save_with_another_search_engine' => [true, false, $dimensions],
148150
'save_with_elasticsearch' => [true, true, []],
149151
'save_with_elasticsearch_and_dimensions' => [true, true, $dimensions],
150152
];
@@ -153,12 +155,12 @@ public function afterSaveDataProvider(): array
153155
/**
154156
* Retrieves how many times method is executed.
155157
*
156-
* @param bool $isInvalid
158+
* @param bool $isNewObject
157159
* @param bool $isElasticsearchEnabled
158160
* @return InvokedCountMatcher
159161
*/
160-
private function getExpectsCount(bool $isInvalid, bool $isElasticsearchEnabled): InvokedCountMatcher
162+
private function getExpectsCount(bool $isNewObject, bool $isElasticsearchEnabled): InvokedCountMatcher
161163
{
162-
return ($isInvalid && $isElasticsearchEnabled) ? $this->once() : $this->never();
164+
return ($isNewObject && $isElasticsearchEnabled) ? $this->once() : $this->never();
163165
}
164166
}

dev/tests/integration/testsuite/Magento/Elasticsearch/Model/Indexer/Fulltext/Plugin/Category/Product/AttributeTest.php

Lines changed: 100 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,15 @@
99

1010
use Magento\AdvancedSearch\Model\Client\ClientInterface;
1111
use Magento\Catalog\Api\Data\EavAttributeInterface;
12-
use Magento\Catalog\Model\Product;
13-
use Magento\Catalog\Model\ResourceModel\Eav\Attribute;
12+
use Magento\Catalog\Api\Data\ProductAttributeInterface;
13+
use Magento\Catalog\Api\ProductAttributeRepositoryInterface;
14+
use Magento\Catalog\Model\ResourceModel\Eav\AttributeFactory;
15+
use Magento\Catalog\Setup\CategorySetup;
1416
use Magento\CatalogSearch\Model\Indexer\Fulltext\Processor;
1517
use Magento\Elasticsearch\Model\Adapter\Index\IndexNameResolver;
1618
use Magento\Elasticsearch\SearchAdapter\ConnectionManager;
1719
use Magento\Framework\Stdlib\ArrayManager;
20+
use Magento\Store\Model\StoreManagerInterface;
1821
use Magento\TestFramework\Helper\Bootstrap;
1922
use PHPUnit\Framework\TestCase;
2023

@@ -44,9 +47,24 @@ class AttributeTest extends TestCase
4447
private $indexerProcessor;
4548

4649
/**
47-
* @var Attribute
50+
* @var StoreManagerInterface
4851
*/
49-
private $attribute;
52+
private $storeManager;
53+
54+
/**
55+
* @var CategorySetup
56+
*/
57+
private $installer;
58+
59+
/**
60+
* @var AttributeFactory
61+
*/
62+
private $attributeFactory;
63+
64+
/**
65+
* @var ProductAttributeRepositoryInterface
66+
*/
67+
private $attributeRepository;
5068

5169
/**
5270
* @inheritdoc
@@ -60,38 +78,59 @@ protected function setUp(): void
6078
$this->arrayManager = Bootstrap::getObjectManager()->get(ArrayManager::class);
6179
$this->indexNameResolver = Bootstrap::getObjectManager()->get(IndexNameResolver::class);
6280
$this->indexerProcessor = Bootstrap::getObjectManager()->get(Processor::class);
63-
$this->attribute = Bootstrap::getObjectManager()->get(Attribute::class);
81+
$this->storeManager = Bootstrap::getObjectManager()->get(StoreManagerInterface::class);
82+
$this->installer = Bootstrap::getObjectManager()->get(CategorySetup::class);
83+
$this->attributeFactory = Bootstrap::getObjectManager()->get(AttributeFactory::class);
84+
$this->attributeRepository = Bootstrap::getObjectManager()->get(ProductAttributeRepositoryInterface::class);
6485
}
6586

6687
/**
67-
* Check Elasticsearch indexer mapping is updated after changing non searchable attribute to searchable.
88+
* @inheritdoc
89+
*/
90+
protected function tearDown(): void
91+
{
92+
parent::tearDown();
93+
94+
/** @var ProductAttributeInterface $attribute */
95+
$attribute = $this->attributeRepository->get('dropdown_attribute');
96+
$this->attributeRepository->delete($attribute);
97+
}
98+
99+
/**
100+
* Check Elasticsearch indexer mapping is updated after creating attribute.
68101
*
69102
* @return void
70103
* @magentoConfigFixture default/catalog/search/engine elasticsearch7
71104
* @magentoDataFixture Magento/CatalogSearch/_files/full_reindex.php
72-
* @magentoDataFixture Magento/Catalog/_files/dropdown_attribute.php
73105
*/
74106
public function testCheckElasticsearchMappingAfterUpdateAttributeToSearchable(): void
75107
{
76108
$mappedAttributesBefore = $this->getMappingProperties();
77-
78-
$this->attribute->loadByCode(Product::ENTITY, 'dropdown_attribute');
79-
$this->attribute->setData(EavAttributeInterface::IS_SEARCHABLE, true)->save();
80-
$this->assertTrue($this->indexerProcessor->getIndexer()->isInvalid());
81-
82-
$mappedAttributesAfter = $this->getMappingProperties();
83109
$expectedResult = [
84110
'dropdown_attribute' => [
85-
'type' => 'keyword',
86-
'copy_to' => ['_search'],
111+
'type' => 'integer',
112+
'index' => false,
87113
],
88114
'dropdown_attribute_value' => [
89115
'type' => 'text',
90116
'copy_to' => ['_search'],
91117
],
92118
];
93119

120+
/** @var ProductAttributeInterface $dropDownAttribute */
121+
$dropDownAttribute = $this->attributeFactory->create();
122+
$dropDownAttribute->setData($this->getAttributeData());
123+
$this->attributeRepository->save($dropDownAttribute);
124+
$this->assertTrue($this->indexerProcessor->getIndexer()->isValid());
125+
126+
$mappedAttributesAfter = $this->getMappingProperties();
94127
$this->assertEquals($expectedResult, array_diff_key($mappedAttributesAfter, $mappedAttributesBefore));
128+
129+
$dropDownAttribute->setData(EavAttributeInterface::IS_SEARCHABLE, true);
130+
$this->attributeRepository->save($dropDownAttribute);
131+
$this->assertTrue($this->indexerProcessor->getIndexer()->isInvalid());
132+
133+
$this->assertEquals($mappedAttributesAfter, $this->getMappingProperties());
95134
}
96135

97136
/**
@@ -101,11 +140,56 @@ public function testCheckElasticsearchMappingAfterUpdateAttributeToSearchable():
101140
*/
102141
private function getMappingProperties(): array
103142
{
143+
$storeId = $this->storeManager->getStore()->getId();
104144
$mappedIndexerId = $this->indexNameResolver->getIndexMapping(Processor::INDEXER_ID);
105-
$indexName = $this->indexNameResolver->getIndexFromAlias(1, $mappedIndexerId);
145+
$indexName = $this->indexNameResolver->getIndexFromAlias($storeId, $mappedIndexerId);
106146
$mappedAttributes = $this->client->getMapping(['index' => $indexName]);
107147
$pathField = $this->arrayManager->findPath('properties', $mappedAttributes);
108148

109149
return $this->arrayManager->get($pathField, $mappedAttributes, []);
110150
}
151+
152+
/**
153+
* Retrieve drop-down attribute data.
154+
*
155+
* @return array
156+
*/
157+
private function getAttributeData(): array
158+
{
159+
$entityTypeId = $this->installer->getEntityTypeId(ProductAttributeInterface::ENTITY_TYPE_CODE);
160+
161+
return [
162+
'attribute_code' => 'dropdown_attribute',
163+
'entity_type_id' => $entityTypeId,
164+
'is_global' => 0,
165+
'is_user_defined' => 1,
166+
'frontend_input' => 'select',
167+
'is_unique' => 0,
168+
'is_required' => 0,
169+
'is_searchable' => 0,
170+
'is_visible_in_advanced_search' => 0,
171+
'is_comparable' => 0,
172+
'is_filterable' => 0,
173+
'is_filterable_in_search' => 0,
174+
'is_used_for_promo_rules' => 0,
175+
'is_html_allowed_on_front' => 1,
176+
'is_visible_on_front' => 1,
177+
'used_in_product_listing' => 1,
178+
'used_for_sort_by' => 0,
179+
'frontend_label' => ['Drop-Down Attribute'],
180+
'backend_type' => 'varchar',
181+
'option' => [
182+
'value' => [
183+
'option_1' => ['Option 1'],
184+
'option_2' => ['Option 2'],
185+
'option_3' => ['Option 3'],
186+
],
187+
'order' => [
188+
'option_1' => 1,
189+
'option_2' => 2,
190+
'option_3' => 3,
191+
],
192+
],
193+
];
194+
}
111195
}

0 commit comments

Comments
 (0)