Skip to content

Commit 77d540c

Browse files
authored
Merge pull request #1662 from algolia/feat/MAGE-1122-intentional-price-indexing
MAGE-1122 intentional price indexing
2 parents 6c8a9cc + 00e4cb1 commit 77d540c

File tree

7 files changed

+264
-35
lines changed

7 files changed

+264
-35
lines changed

Helper/ConfigHelper.php

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

112113
public const SHOW_OUT_OF_STOCK = 'cataloginventory/options/show_out_of_stock';
113114

@@ -1272,6 +1273,15 @@ public function getWriteTimeout($storeId = null)
12721273
return $this->configInterface->getValue(self::WRITE_TIMEOUT, ScopeInterface::SCOPE_STORE, $storeId);
12731274
}
12741275

1276+
public function isAutoPriceIndexingEnabled(?int $storeId = null): bool
1277+
{
1278+
return $this->configInterface->isSetFlag(
1279+
self::AUTO_PRICE_INDEXING_ENABLED,
1280+
ScopeInterface::SCOPE_STORE,
1281+
$storeId
1282+
);
1283+
}
1284+
12751285
/**
12761286
* @param $storeId
12771287
* @return array|bool|float|int|mixed|string

Helper/Data.php

Lines changed: 24 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
use Algolia\AlgoliaSearch\Helper\Entity\ProductHelper;
1313
use Algolia\AlgoliaSearch\Helper\Entity\SuggestionHelper;
1414
use Algolia\AlgoliaSearch\Service\IndexNameFetcher;
15+
use Algolia\AlgoliaSearch\Service\Product\MissingPriceIndexHandler;
1516
use Magento\Catalog\Model\Category;
1617
use Magento\Catalog\Model\Product;
1718
use Magento\Catalog\Model\ResourceModel\Product\Collection;
@@ -35,21 +36,22 @@ class Data
3536
protected IndexerInterface $priceIndexer;
3637

3738
public function __construct(
38-
protected AlgoliaHelper $algoliaHelper,
39-
protected ConfigHelper $configHelper,
40-
protected ProductHelper $productHelper,
41-
protected CategoryHelper $categoryHelper,
42-
protected PageHelper $pageHelper,
43-
protected SuggestionHelper $suggestionHelper,
44-
protected AdditionalSectionHelper $additionalSectionHelper,
45-
protected Emulation $emulation,
46-
protected Logger $logger,
47-
protected ResourceConnection $resource,
48-
protected ManagerInterface $eventManager,
49-
protected ScopeCodeResolver $scopeCodeResolver,
50-
protected StoreManagerInterface $storeManager,
51-
protected IndexNameFetcher $indexNameFetcher,
52-
IndexerRegistry $indexerRegistry
39+
protected AlgoliaHelper $algoliaHelper,
40+
protected ConfigHelper $configHelper,
41+
protected ProductHelper $productHelper,
42+
protected CategoryHelper $categoryHelper,
43+
protected PageHelper $pageHelper,
44+
protected SuggestionHelper $suggestionHelper,
45+
protected AdditionalSectionHelper $additionalSectionHelper,
46+
protected Emulation $emulation,
47+
protected Logger $logger,
48+
protected ResourceConnection $resource,
49+
protected ManagerInterface $eventManager,
50+
protected ScopeCodeResolver $scopeCodeResolver,
51+
protected StoreManagerInterface $storeManager,
52+
protected IndexNameFetcher $indexNameFetcher,
53+
protected MissingPriceIndexHandler $missingPriceIndexHandler,
54+
IndexerRegistry $indexerRegistry
5355
)
5456
{
5557
$this->priceIndexer = $indexerRegistry->get('catalog_product_price');
@@ -78,7 +80,7 @@ public function deleteObjects(int $storeId, array $ids, string $indexName): void
7880
$this->algoliaHelper->deleteObjects($ids, $indexName);
7981
}
8082

81-
/**
83+
/**`
8284
* @param string $query
8385
* @param int $storeId
8486
* @param array|null $searchParams
@@ -370,8 +372,6 @@ public function rebuildStoreProductIndex(int $storeId, array $productIds): void
370372
return;
371373
}
372374

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

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
</group>
13411351
<group id="queue" translate="label" type="text" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="1">
13421352
<label>Indexing Queue</label>

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
</advanced>
9899
<queue>
99100
<number_of_element_by_page>300</number_of_element_by_page>

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)