Skip to content

Commit 4bf715d

Browse files
authored
Merge pull request #1665 from algolia/feat/MAGE-1122-intentional-price-indexing-3.15
MAGE-1122 Refactor intentional price indexing for 3.15
2 parents bc47132 + 731132c commit 4bf715d

File tree

9 files changed

+266
-21
lines changed

9 files changed

+266
-21
lines changed

Controller/Adminhtml/Reindex/Save.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
use Magento\Backend\App\Action\Context;
1414
use Magento\Catalog\Api\ProductRepositoryInterface;
1515
use Magento\Framework\Controller\ResultFactory;
16+
use Magento\Framework\Exception\LocalizedException;
1617
use Magento\Framework\Exception\NoSuchEntityException;
1718
use Magento\Store\Model\StoreManagerInterface;
1819

@@ -110,6 +111,7 @@ public function execute()
110111
* @param $stores
111112
* @return void
112113
* @throws NoSuchEntityException
114+
* @throws LocalizedException
113115
*/
114116
protected function checkAndReindex($product, $stores)
115117
{
@@ -199,7 +201,7 @@ protected function checkAndReindex($product, $stores)
199201
$productIds = [$product->getId()];
200202
$productIds = array_merge($productIds, $this->productHelper->getParentProductIds($productIds));
201203

202-
$this->productIndexBuilder->rebuildEntityIds($storeId, $productIds);
204+
$this->productIndexBuilder->buildIndexList($storeId, $productIds);
203205
$this->messageManager->addSuccessMessage(
204206
__(
205207
'The Product "%1" (%2) has been reindexed for store "%3 / %4 / %5".',

Helper/ConfigHelper.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ class ConfigHelper
109109
public const CONNECTION_TIMEOUT = 'algoliasearch_advanced/advanced/connection_timeout';
110110
public const READ_TIMEOUT = 'algoliasearch_advanced/advanced/read_timeout';
111111
public const WRITE_TIMEOUT = 'algoliasearch_advanced/advanced/write_timeout';
112+
public const AUTO_PRICE_INDEXING_ENABLED = 'algoliasearch_advanced/advanced/auto_price_indexing';
112113

113114
public const PROFILER_ENABLED = 'algoliasearch_advanced/advanced/enable_profiler';
114115

@@ -1285,6 +1286,15 @@ public function getWriteTimeout($storeId = null)
12851286
return $this->configInterface->getValue(self::WRITE_TIMEOUT, ScopeInterface::SCOPE_STORE, $storeId);
12861287
}
12871288

1289+
public function isAutoPriceIndexingEnabled(?int $storeId = null): bool
1290+
{
1291+
return $this->configInterface->isSetFlag(
1292+
self::AUTO_PRICE_INDEXING_ENABLED,
1293+
ScopeInterface::SCOPE_STORE,
1294+
$storeId
1295+
);
1296+
}
1297+
12881298
/**
12891299
* @param $storeId
12901300
* @return array|bool|float|int|mixed|string

Helper/Entity/ProductHelper.php

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1333,7 +1333,6 @@ protected function clearFacetsQueryRules($indexName, int $storeId = null): void
13331333
*/
13341334
public function canProductBeReindexed($product, $storeId, $isChildProduct = false)
13351335
{
1336-
$this->logger->startProfiling(__METHOD__);
13371336
if ($product->isDeleted() === true) {
13381337
throw (new ProductDeletedException())
13391338
->withProduct($product)
@@ -1367,7 +1366,6 @@ public function canProductBeReindexed($product, $storeId, $isChildProduct = fals
13671366
->withStoreId($storeId);
13681367
}
13691368

1370-
$this->logger->stopProfiling(__METHOD__);
13711369
return true;
13721370
}
13731371

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
namespace Algolia\AlgoliaSearch\Model\Config;
4+
5+
use Magento\Config\Model\Config\CommentInterface;
6+
use Magento\Framework\UrlInterface;
7+
8+
class AutomaticPriceIndexingComment implements CommentInterface
9+
{
10+
public function __construct(
11+
protected UrlInterface $urlInterface
12+
) { }
13+
14+
public function getCommentText($elementValue)
15+
{
16+
$url = $this->urlInterface->getUrl('https://www.algolia.com/doc/integration/magento-2/how-it-works/indexing-queue/#configure-the-queue');
17+
18+
$comment = array();
19+
$comment[] = 'Algolia relies on the core Magento Product Price index when serializing product data. If the price index is not up to date, Algolia will not be able to accurately determine what should be included in the search index.';
20+
$comment[] = 'If you are experiencing problems with products not syncing to Algolia due to this issue, enabling this setting will allow Algolia to automatically update the price index as needed.';
21+
$comment[] = 'NOTE: This can introduce a marginal amount of overhead to the indexing process so only enable if necessary. Be sure to <a href="' . $url . '" target="_blank">optimize the indexing queue</a> based on the impact of this operation.';
22+
return implode('<br><br>', $comment);
23+
}
24+
}

Service/Product/IndexBuilder.php

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -27,15 +27,16 @@ class IndexBuilder extends AbstractIndexBuilder implements UpdatableIndexBuilder
2727
protected IndexerInterface $priceIndexer;
2828

2929
public function __construct(
30-
protected ConfigHelper $configHelper,
31-
protected DiagnosticsLogger $logger,
32-
protected Emulation $emulation,
33-
protected ScopeCodeResolver $scopeCodeResolver,
34-
protected AlgoliaHelper $algoliaHelper,
35-
protected ProductHelper $productHelper,
36-
protected ResourceConnection $resource,
37-
protected ManagerInterface $eventManager,
38-
IndexerRegistry $indexerRegistry
30+
protected ConfigHelper $configHelper,
31+
protected DiagnosticsLogger $logger,
32+
protected Emulation $emulation,
33+
protected ScopeCodeResolver $scopeCodeResolver,
34+
protected AlgoliaHelper $algoliaHelper,
35+
protected ProductHelper $productHelper,
36+
protected ResourceConnection $resource,
37+
protected ManagerInterface $eventManager,
38+
protected MissingPriceIndexHandler $missingPriceIndexHandler,
39+
IndexerRegistry $indexerRegistry
3940
){
4041
parent::__construct($configHelper, $logger, $emulation, $scopeCodeResolver, $algoliaHelper);
4142

@@ -107,12 +108,6 @@ public function buildIndex(int $storeId, ?array $entityIds, ?array $options): vo
107108
*/
108109
protected function rebuildEntityIds(int $storeId, array $productIds): void
109110
{
110-
if ($this->isIndexingEnabled($storeId) === false) {
111-
return;
112-
}
113-
114-
$this->checkPriceIndex($productIds);
115-
116111
$this->startEmulation($storeId);
117112
$this->logger->start('Indexing');
118113
try {
@@ -236,6 +231,11 @@ protected function buildIndexPage(
236231
'store' => $storeId
237232
]
238233
);
234+
235+
if ($this->configHelper->isAutoPriceIndexingEnabled($storeId)) {
236+
$this->missingPriceIndexHandler->refreshPriceIndex($collection);
237+
}
238+
239239
$logMessage = 'LOADING: ' . $this->logger->getStoreName($storeId) . ',
240240
collection page: ' . $page . ',
241241
pageSize: ' . $pageSize;
@@ -322,10 +322,13 @@ protected function getProductsRecords($storeId, $collection, $potentiallyDeleted
322322
}
323323

324324
try {
325+
$this->logger->startProfiling("canProductBeReindexed");
325326
$this->productHelper->canProductBeReindexed($product, $storeId);
326327
} catch (ProductReindexingException $e) {
327328
$productsToRemove[$productId] = $productId;
328329
continue;
330+
} finally {
331+
$this->logger->stopProfiling("canProductBeReindexed");
329332
}
330333

331334
if (isset($salesData[$productId])) {
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
<?php
2+
3+
namespace Algolia\AlgoliaSearch\Service\Product;
4+
5+
use Algolia\AlgoliaSearch\Exception\DiagnosticsException;
6+
use Algolia\AlgoliaSearch\Logger\DiagnosticsLogger;
7+
use Magento\Catalog\Model\ResourceModel\Product\Collection as ProductCollection;
8+
use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory;
9+
use Magento\Framework\App\ResourceConnection;
10+
use Magento\Framework\DB\Select;
11+
use Magento\Framework\Indexer\IndexerInterface;
12+
use Magento\Framework\Indexer\IndexerRegistry;
13+
use Magento\Framework\Indexer\StateInterface;
14+
use Zend_Db_Select;
15+
16+
class MissingPriceIndexHandler
17+
{
18+
public const PRICE_INDEX_TABLE = 'catalog_product_index_price';
19+
public const PRICE_INDEX_TABLE_ALIAS = 'price_index';
20+
public const MAIN_TABLE_ALIAS = 'e';
21+
22+
protected array $_indexedProducts = [];
23+
24+
protected IndexerInterface $indexer;
25+
public function __construct(
26+
protected CollectionFactory $productCollectionFactory,
27+
protected ResourceConnection $resourceConnection,
28+
protected DiagnosticsLogger $diagnostics,
29+
IndexerRegistry $indexerRegistry
30+
)
31+
{
32+
$this->indexer = $indexerRegistry->get('catalog_product_price');
33+
}
34+
35+
/**
36+
* @param string[]|ProductCollection $products
37+
* @return string[] Array of product IDs that were reindexed by this repair operation
38+
* @throws DiagnosticsException
39+
*/
40+
public function refreshPriceIndex(array|ProductCollection $products): array
41+
{
42+
$this->diagnostics->startProfiling(__METHOD__);
43+
$reindexIds = $this->getProductIdsToReindex($products);
44+
if (empty($reindexIds)) {
45+
$this->diagnostics->stopProfiling(__METHOD__);
46+
return [];
47+
}
48+
49+
$this->diagnostics->log(__("Pricing records missing or invalid for %1 product(s)", count($reindexIds)));
50+
$this->diagnostics->log(__("Reindexing product ID(s): %1", implode(', ', $reindexIds)));
51+
52+
$this->indexer->reindexList($reindexIds);
53+
54+
$this->diagnostics->stopProfiling(__METHOD__);
55+
return $reindexIds;
56+
}
57+
58+
/**
59+
* Analyzes a product collection and determines which (if any) records should have their prices reindexed
60+
* @param string[]|ProductCollection $products - either an explicit list of product ids or a product collection
61+
* @return string[] IDs of products that require price reindexing (will be empty if no indexing is required)
62+
*/
63+
protected function getProductIdsToReindex(array|ProductCollection $products): array
64+
{
65+
$productIds = $products instanceof ProductCollection
66+
? $this->getProductIdsFromCollection($products)
67+
: $products;
68+
69+
if (empty($productIds)) {
70+
return [];
71+
}
72+
73+
$state = $this->indexer->getState()->getStatus();
74+
if ($state === StateInterface::STATUS_INVALID) {
75+
return $this->filterProductIdsNotYetProcessed($productIds);
76+
}
77+
78+
$productIds = $this->filterProductIdsMissingPricing($productIds);
79+
if (empty($productIds)) {
80+
return [];
81+
}
82+
83+
return $this->filterProductIdsNotYetProcessed($productIds);
84+
}
85+
86+
protected function filterProductIdsMissingPricing(array $productIds): array
87+
{
88+
$collection = $this->productCollectionFactory->create();
89+
90+
$collection->addAttributeToSelect(['name', 'price']);
91+
92+
$collection->getSelect()->joinLeft(
93+
[self::PRICE_INDEX_TABLE_ALIAS => self::PRICE_INDEX_TABLE],
94+
self::MAIN_TABLE_ALIAS . '.entity_id = ' . self::PRICE_INDEX_TABLE_ALIAS . '.entity_id',
95+
[]
96+
);
97+
98+
$collection->getSelect()
99+
->where(self::PRICE_INDEX_TABLE_ALIAS . '.entity_id IS NULL')
100+
->where(self::MAIN_TABLE_ALIAS . '.entity_id IN (?)', $productIds);
101+
102+
return $collection->getAllIds();
103+
}
104+
105+
protected function filterProductIdsNotYetProcessed(array $productIds): array {
106+
$pendingProcessing = array_fill_keys($productIds, true);
107+
108+
$notProcessed = array_diff_key($pendingProcessing, $this->_indexedProducts);
109+
110+
if (empty($notProcessed)) {
111+
return [];
112+
}
113+
114+
$this->_indexedProducts += $notProcessed;
115+
116+
return array_keys($notProcessed);
117+
}
118+
119+
/**
120+
* Expand the query for product ids from the collection regardless of price index status
121+
* @return string[] An array of indices to be evaluated - array will be empty if no price index join found
122+
*/
123+
protected function getProductIdsFromCollection(ProductCollection $collection): array
124+
{
125+
126+
$select = clone $collection->getSelect();
127+
try {
128+
$joins = $select->getPart(Zend_Db_Select::FROM);
129+
} catch (\Zend_Db_Select_Exception $e) {
130+
$this->diagnostics->error("Unable to build query for missing product prices: " . $e->getMessage());
131+
return [];
132+
}
133+
134+
$priceIndexJoin = $this->getPriceIndexJoinAlias($joins);
135+
136+
if (!$priceIndexJoin) {
137+
// no price index on query - keep calm and carry on
138+
return [];
139+
}
140+
141+
$this->expandPricingJoin($joins, $priceIndexJoin);
142+
$this->rebuildJoins($select, $joins);
143+
144+
return $this->resourceConnection->getConnection()->fetchCol($select);
145+
}
146+
147+
protected function expandPricingJoin(array &$joins, string $priceIndexJoin): void
148+
{
149+
$modifyJoin = &$joins[$priceIndexJoin];
150+
$modifyJoin['joinType'] = Zend_Db_Select::LEFT_JOIN;
151+
}
152+
153+
protected function rebuildJoins(Select $select, array $joins): void
154+
{
155+
$select->reset(Zend_Db_Select::COLUMNS);
156+
$select->reset(Zend_Db_Select::FROM);
157+
foreach ($joins as $alias => $joinData) {
158+
if ($joinData['joinType'] === Zend_Db_Select::FROM) {
159+
$select->from(
160+
[$alias => $joinData['tableName']],
161+
'entity_id'
162+
);
163+
} elseif ($joinData['joinType'] === Zend_Db_Select::LEFT_JOIN) {
164+
$select->joinLeft(
165+
[$alias => $joinData['tableName']],
166+
$joinData['joinCondition'],
167+
[],
168+
$joinData['schema']
169+
);
170+
} else {
171+
$select->join(
172+
[$alias => $joinData['tableName']],
173+
$joinData['joinCondition'],
174+
[],
175+
$joinData['schema']
176+
);
177+
}
178+
}
179+
}
180+
181+
/**
182+
* @param array<string, array> $joins
183+
* @return string
184+
*/
185+
protected function getPriceIndexJoinAlias(array $joins): string
186+
{
187+
if (isset($joins[self::PRICE_INDEX_TABLE_ALIAS])) {
188+
return self::PRICE_INDEX_TABLE_ALIAS;
189+
}
190+
else {
191+
foreach ($joins as $alias => $joinData) {
192+
if ($joinData['tableName'] === self::PRICE_INDEX_TABLE) {
193+
return $alias;
194+
}
195+
}
196+
}
197+
198+
return "";
199+
}
200+
}

etc/adminhtml/system.xml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1337,6 +1337,16 @@
13371337
<field id="write_timeout" translate="label comment" type="text" sortOrder="110" showInDefault="1">
13381338
<label>Write Timeout (In Seconds)</label>
13391339
</field>
1340+
<field id="auto_price_indexing" translate="label comment" type="select" sortOrder="120" showInDefault="1" showInWebsite="1" showInStore="1">
1341+
<label>Enable automatic price indexing</label>
1342+
<source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
1343+
<comment>
1344+
<model>Algolia\AlgoliaSearch\Model\Config\AutomaticPriceIndexingComment</model>
1345+
</comment>
1346+
<depends>
1347+
<field id="active">1</field>
1348+
</depends>
1349+
</field>
13401350
<field id="enable_profiler" translate="label comment" type="select" sortOrder="130" showInDefault="1">
13411351
<label>Enable Profiler</label>
13421352
<source_model>Magento\Config\Model\Config\Source\Yesno</source_model>

etc/config.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@
9494
<connection_timeout>2</connection_timeout>
9595
<read_timeout>30</read_timeout>
9696
<write_timeout>30</write_timeout>
97+
<auto_price_indexing>0</auto_price_indexing>
9798
<enable_profiler>0</enable_profiler>
9899
</advanced>
99100
<queue>

etc/indexer.xml

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,6 @@
55
<description translate="true">
66
Rebuild products index.
77
</description>
8-
<dependencies>
9-
<indexer id="catalog_product_price" />
10-
</dependencies>
118
</indexer>
129
<indexer id="algolia_categories" view_id="algolia_categories" class="Algolia\AlgoliaSearch\Model\Indexer\Category">
1310
<title translate="true">Algolia Search Categories</title>

0 commit comments

Comments
 (0)