Skip to content

Commit

Permalink
Merge pull request #1662 from algolia/feat/MAGE-1122-intentional-pric…
Browse files Browse the repository at this point in the history
…e-indexing

MAGE-1122 intentional price indexing
  • Loading branch information
cammonro authored Dec 18, 2024
2 parents 6c8a9cc + 00e4cb1 commit 77d540c
Show file tree
Hide file tree
Showing 7 changed files with 264 additions and 35 deletions.
10 changes: 10 additions & 0 deletions Helper/ConfigHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ class ConfigHelper
public const CONNECTION_TIMEOUT = 'algoliasearch_advanced/advanced/connection_timeout';
public const READ_TIMEOUT = 'algoliasearch_advanced/advanced/read_timeout';
public const WRITE_TIMEOUT = 'algoliasearch_advanced/advanced/write_timeout';
public const AUTO_PRICE_INDEXING_ENABLED = 'algoliasearch_advanced/advanced/auto_price_indexing';

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

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

public function isAutoPriceIndexingEnabled(?int $storeId = null): bool
{
return $this->configInterface->isSetFlag(
self::AUTO_PRICE_INDEXING_ENABLED,
ScopeInterface::SCOPE_STORE,
$storeId
);
}

/**
* @param $storeId
* @return array|bool|float|int|mixed|string
Expand Down
56 changes: 24 additions & 32 deletions Helper/Data.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use Algolia\AlgoliaSearch\Helper\Entity\ProductHelper;
use Algolia\AlgoliaSearch\Helper\Entity\SuggestionHelper;
use Algolia\AlgoliaSearch\Service\IndexNameFetcher;
use Algolia\AlgoliaSearch\Service\Product\MissingPriceIndexHandler;
use Magento\Catalog\Model\Category;
use Magento\Catalog\Model\Product;
use Magento\Catalog\Model\ResourceModel\Product\Collection;
Expand All @@ -35,21 +36,22 @@ class Data
protected IndexerInterface $priceIndexer;

public function __construct(
protected AlgoliaHelper $algoliaHelper,
protected ConfigHelper $configHelper,
protected ProductHelper $productHelper,
protected CategoryHelper $categoryHelper,
protected PageHelper $pageHelper,
protected SuggestionHelper $suggestionHelper,
protected AdditionalSectionHelper $additionalSectionHelper,
protected Emulation $emulation,
protected Logger $logger,
protected ResourceConnection $resource,
protected ManagerInterface $eventManager,
protected ScopeCodeResolver $scopeCodeResolver,
protected StoreManagerInterface $storeManager,
protected IndexNameFetcher $indexNameFetcher,
IndexerRegistry $indexerRegistry
protected AlgoliaHelper $algoliaHelper,
protected ConfigHelper $configHelper,
protected ProductHelper $productHelper,
protected CategoryHelper $categoryHelper,
protected PageHelper $pageHelper,
protected SuggestionHelper $suggestionHelper,
protected AdditionalSectionHelper $additionalSectionHelper,
protected Emulation $emulation,
protected Logger $logger,
protected ResourceConnection $resource,
protected ManagerInterface $eventManager,
protected ScopeCodeResolver $scopeCodeResolver,
protected StoreManagerInterface $storeManager,
protected IndexNameFetcher $indexNameFetcher,
protected MissingPriceIndexHandler $missingPriceIndexHandler,
IndexerRegistry $indexerRegistry
)
{
$this->priceIndexer = $indexerRegistry->get('catalog_product_price');
Expand Down Expand Up @@ -78,7 +80,7 @@ public function deleteObjects(int $storeId, array $ids, string $indexName): void
$this->algoliaHelper->deleteObjects($ids, $indexName);
}

/**
/**`
* @param string $query
* @param int $storeId
* @param array|null $searchParams
Expand Down Expand Up @@ -370,8 +372,6 @@ public function rebuildStoreProductIndex(int $storeId, array $productIds): void
return;
}

$this->checkPriceIndex($productIds);

$this->startEmulation($storeId);
$this->logger->start('Indexing');
try {
Expand Down Expand Up @@ -686,6 +686,7 @@ public function rebuildStoreProductIndexPage(
page ' . $page . ',
pageSize ' . $pageSize;
$this->logger->start($wrapperLogMessage);

if ($emulationInfo === null) {
$this->startEmulation($storeId);
}
Expand All @@ -711,6 +712,11 @@ public function rebuildStoreProductIndexPage(
'store' => $storeId
]
);

if ($this->configHelper->isAutoPriceIndexingEnabled($storeId)) {
$this->missingPriceIndexHandler->refreshPriceIndex($collection);
}

$logMessage = 'LOADING: ' . $this->logger->getStoreName($storeId) . ',
collection page: ' . $page . ',
pageSize: ' . $pageSize;
Expand Down Expand Up @@ -936,18 +942,4 @@ protected function deleteInactiveIds($storeId, $objectIds, $indexName): void
$idsToDeleteFromAlgolia = array_diff($objectIds, $dbIds);
$this->algoliaHelper->deleteObjects($idsToDeleteFromAlgolia, $indexName);
}

/**
* If the price index is stale
* @param array $productIds
* @return void
*/
protected function checkPriceIndex(array $productIds): void
{
$state = $this->priceIndexer->getState()->getStatus();
if ($state === \Magento\Framework\Indexer\StateInterface::STATUS_INVALID) {
$this->priceIndexer->reindexList($productIds);
}
}

}
24 changes: 24 additions & 0 deletions Model/Config/AutomaticPriceIndexingComment.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

namespace Algolia\AlgoliaSearch\Model\Config;

use Magento\Config\Model\Config\CommentInterface;
use Magento\Framework\UrlInterface;

class AutomaticPriceIndexingComment implements CommentInterface
{
public function __construct(
protected UrlInterface $urlInterface
) { }

public function getCommentText($elementValue)
{
$url = $this->urlInterface->getUrl('https://www.algolia.com/doc/integration/magento-2/how-it-works/indexing-queue/#configure-the-queue');

$comment = array();
$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.';
$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.';
$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.';
return implode('<br><br>', $comment);
}
}
195 changes: 195 additions & 0 deletions Service/Product/MissingPriceIndexHandler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
<?php

namespace Algolia\AlgoliaSearch\Service\Product;

use Algolia\AlgoliaSearch\Helper\Logger;
use Magento\Catalog\Model\ResourceModel\Product\Collection as ProductCollection;
use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory;
use Magento\Framework\App\ResourceConnection;
use Magento\Framework\DB\Select;
use Magento\Framework\Indexer\IndexerInterface;
use Magento\Framework\Indexer\IndexerRegistry;
use Magento\Framework\Indexer\StateInterface;
use Zend_Db_Select;

class MissingPriceIndexHandler
{
public const PRICE_INDEX_TABLE = 'catalog_product_index_price';
public const PRICE_INDEX_TABLE_ALIAS = 'price_index';
public const MAIN_TABLE_ALIAS = 'e';

protected array $_indexedProducts = [];

protected IndexerInterface $indexer;
public function __construct(
protected CollectionFactory $productCollectionFactory,
protected ResourceConnection $resourceConnection,
protected Logger $logger,
IndexerRegistry $indexerRegistry
)
{
$this->indexer = $indexerRegistry->get('catalog_product_price');
}

/**
* @param string[]|ProductCollection $products
* @return string[] Array of product IDs that were reindexed by this repair operation
*/
public function refreshPriceIndex(array|ProductCollection $products): array
{
$reindexIds = $this->getProductIdsToReindex($products);
if (empty($reindexIds)) {
return [];
}

$this->logger->log(__("Pricing records missing or invalid for %1 product(s)", count($reindexIds)));
$this->logger->log(__("Reindexing product ID(s): %1", implode(', ', $reindexIds)));

$this->indexer->reindexList($reindexIds);

return $reindexIds;
}

/**
* Analyzes a product collection and determines which (if any) records should have their prices reindexed
* @param string[]|ProductCollection $products - either an explicit list of product ids or a product collection
* @return string[] IDs of products that require price reindexing (will be empty if no indexing is required)
*/
protected function getProductIdsToReindex(array|ProductCollection $products): array
{
$productIds = $products instanceof ProductCollection
? $this->getProductIdsFromCollection($products)
: $products;

if (empty($productIds)) {
return [];
}

$state = $this->indexer->getState()->getStatus();
if ($state === StateInterface::STATUS_INVALID) {
return $this->filterProductIdsNotYetProcessed($productIds);
}

$productIds = $this->filterProductIdsMissingPricing($productIds);
if (empty($productIds)) {
return [];
}

return $this->filterProductIdsNotYetProcessed($productIds);
}

protected function filterProductIdsMissingPricing(array $productIds): array
{
$collection = $this->productCollectionFactory->create();

$collection->addAttributeToSelect(['name', 'price']);

$collection->getSelect()->joinLeft(
[self::PRICE_INDEX_TABLE_ALIAS => self::PRICE_INDEX_TABLE],
self::MAIN_TABLE_ALIAS . '.entity_id = ' . self::PRICE_INDEX_TABLE_ALIAS . '.entity_id',
[]
);

$collection->getSelect()
->where(self::PRICE_INDEX_TABLE_ALIAS . '.entity_id IS NULL')
->where(self::MAIN_TABLE_ALIAS . '.entity_id IN (?)', $productIds);

return $collection->getAllIds();
}

protected function filterProductIdsNotYetProcessed(array $productIds): array {
$pendingProcessing = array_fill_keys($productIds, true);

$notProcessed = array_diff_key($pendingProcessing, $this->_indexedProducts);

if (empty($notProcessed)) {
return [];
}

$this->_indexedProducts += $notProcessed;

return array_keys($notProcessed);
}

/**
* Expand the query for product ids from the collection regardless of price index status
* @return string[] An array of indices to be evaluated - array will be empty if no price index join found
*/
protected function getProductIdsFromCollection(ProductCollection $collection): array
{

$select = clone $collection->getSelect();
try {
$joins = $select->getPart(Zend_Db_Select::FROM);
} catch (\Zend_Db_Select_Exception $e) {
$this->logger->error("Unable to build query for missing product prices: " . $e->getMessage());
return [];
}

$priceIndexJoin = $this->getPriceIndexJoinAlias($joins);

if (!$priceIndexJoin) {
// no price index on query - keep calm and carry on
return [];
}

$this->expandPricingJoin($joins, $priceIndexJoin);
$this->rebuildJoins($select, $joins);

return $this->resourceConnection->getConnection()->fetchCol($select);
}

protected function expandPricingJoin(array &$joins, string $priceIndexJoin): void
{
$modifyJoin = &$joins[$priceIndexJoin];
$modifyJoin['joinType'] = Zend_Db_Select::LEFT_JOIN;
}

protected function rebuildJoins(Select $select, array $joins): void
{
$select->reset(Zend_Db_Select::COLUMNS);
$select->reset(Zend_Db_Select::FROM);
foreach ($joins as $alias => $joinData) {
if ($joinData['joinType'] === Zend_Db_Select::FROM) {
$select->from(
[$alias => $joinData['tableName']],
'entity_id'
);
} elseif ($joinData['joinType'] === Zend_Db_Select::LEFT_JOIN) {
$select->joinLeft(
[$alias => $joinData['tableName']],
$joinData['joinCondition'],
[],
$joinData['schema']
);
} else {
$select->join(
[$alias => $joinData['tableName']],
$joinData['joinCondition'],
[],
$joinData['schema']
);
}
}
}

/**
* @param array<string, array> $joins
* @return string
*/
protected function getPriceIndexJoinAlias(array $joins): string
{
if (isset($joins[self::PRICE_INDEX_TABLE_ALIAS])) {
return self::PRICE_INDEX_TABLE_ALIAS;
}
else {
foreach ($joins as $alias => $joinData) {
if ($joinData['tableName'] === self::PRICE_INDEX_TABLE) {
return $alias;
}
}
}

return "";
}
}
10 changes: 10 additions & 0 deletions etc/adminhtml/system.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1337,6 +1337,16 @@
<field id="write_timeout" translate="label comment" type="text" sortOrder="110" showInDefault="1">
<label>Write Timeout (In Seconds)</label>
</field>
<field id="auto_price_indexing" translate="label comment" type="select" sortOrder="120" showInDefault="1" showInWebsite="1" showInStore="1">
<label>Enable automatic price indexing</label>
<source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
<comment>
<model>Algolia\AlgoliaSearch\Model\Config\AutomaticPriceIndexingComment</model>
</comment>
<depends>
<field id="active">1</field>
</depends>
</field>
</group>
<group id="queue" translate="label" type="text" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="1">
<label>Indexing Queue</label>
Expand Down
1 change: 1 addition & 0 deletions etc/config.xml
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@
<connection_timeout>2</connection_timeout>
<read_timeout>30</read_timeout>
<write_timeout>30</write_timeout>
<auto_price_indexing>0</auto_price_indexing>
</advanced>
<queue>
<number_of_element_by_page>300</number_of_element_by_page>
Expand Down
3 changes: 0 additions & 3 deletions etc/indexer.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,6 @@
<description translate="true">
Rebuild products index.
</description>
<dependencies>
<indexer id="catalog_product_price" />
</dependencies>
</indexer>
<indexer id="algolia_categories" view_id="algolia_categories" class="Algolia\AlgoliaSearch\Model\Indexer\Category">
<title translate="true">Algolia Search Categories</title>
Expand Down

0 comments on commit 77d540c

Please sign in to comment.