Skip to content

Commit 7418962

Browse files
authored
Merge pull request #1531 from algolia/feature/MAGE-721-v2
MAGE-721 (including MAGE-835 MAGE-836 MAGE-840) Refactor for granular virtual replicas
2 parents 2ab76e3 + 72c3953 commit 7418962

21 files changed

+859
-450
lines changed
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
namespace Algolia\AlgoliaSearch\Api\Product;
4+
5+
use Algolia\AlgoliaSearch\Exceptions\AlgoliaException;
6+
use Algolia\AlgoliaSearch\Exceptions\ExceededRetriesException;
7+
use Magento\Framework\Exception\LocalizedException;
8+
use Magento\Framework\Exception\NoSuchEntityException;
9+
10+
interface ReplicaManagerInterface
11+
{
12+
/**
13+
* Configure replicas in Algolia based on the sorting configuration in Magento
14+
*
15+
* @param string $indexName Could be tmp (legacy impl)
16+
* @param int $storeId
17+
* @param array<string, mixed> $primaryIndexSettings
18+
* @return void
19+
*
20+
* @throws AlgoliaException
21+
* @throws ExceededRetriesException
22+
* @throws LocalizedException
23+
* @throws NoSuchEntityException
24+
*/
25+
public function handleReplicas(string $indexName, int $storeId, array $primaryIndexSettings): void;
26+
}

CHANGELOG.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,37 @@
11
# CHANGE LOG
22

3+
## 3.14.0-beta.2
4+
5+
### Updates
6+
7+
- Introduced new admin groups to InstantSearch for improved UX
8+
- Updated `ConfigHelper` to use new paths
9+
- Added data patch to migrate old configurations
10+
- Bugfix for query rule disable on facets with new admin groupings
11+
- Added new sorting admin option via source model
12+
- Added derived virtual replica enablement to `ConfigHelper` based on `ArraySerialized`
13+
- Intro’d simplified data structures to avoid array diff mismatches
14+
- Intro’d new `ReplicaManager` abstraction to map Magento sorting to Algolia replica configuration
15+
- Removed dependencies in backend models to handle replica config updates in Algolia addressing stale data
16+
- Added `ReplicaState` registry for tracking changes to sorting configuration to minimize number of replica build operations
17+
- Added logic to preserve replicas created outside of Magento such as Merchandising Studio "sorting strategies"
18+
- Introduced PHP 8 constructor property promotion on affected classes
19+
- Added stronger typing to affected classes and methods
20+
21+
## 3.14.0-beta.1
22+
23+
### Updates:
24+
25+
- New PHP API client (v4) under the hood for communicating with Algolia
26+
- Authenticated user tokens now utilized for backend and frontend events to track entire customer journey
27+
- Revenue data now sent with all events including application of Magento specific discounts such as catalog price rules and customer group pricing
28+
- Support for event subtypes allowing the capture of conversion data for both "Add to cart" and "Place order" events
29+
- Increased protection of PII in the event data
30+
31+
### Bug fixes:
32+
- Fixed issue with how Algolia extension handles end user consent for allowing cookies
33+
- Improved handling of user tokens across insights events and corresponding queries
34+
335
## 3.13.3
436

537
### Updates

Helper/ConfigHelper.php

Lines changed: 152 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use Magento\Directory\Model\Currency as DirCurrency;
88
use Magento\Framework\App\Filesystem\DirectoryList;
99
use Magento\Framework\DataObject;
10+
use Magento\Framework\Exception\LocalizedException;
1011
use Magento\Framework\Locale\Currency;
1112
use Magento\Framework\Serialize\SerializerInterface;
1213
use Magento\Store\Model\ScopeInterface;
@@ -35,14 +36,14 @@ class ConfigHelper
3536
public const REPLACE_CATEGORIES = 'algoliasearch_instant/instant/replace_categories';
3637
public const INSTANT_SELECTOR = 'algoliasearch_instant/instant/instant_selector';
3738
public const NUMBER_OF_PRODUCT_RESULTS = 'algoliasearch_instant/instant/number_product_results';
38-
public const FACETS = 'algoliasearch_instant/instant/facets';
39-
public const MAX_VALUES_PER_FACET = 'algoliasearch_instant/instant/max_values_per_facet';
40-
public const SORTING_INDICES = 'algoliasearch_instant/instant/sorts';
41-
public const SHOW_SUGGESTIONS_NO_RESULTS = 'algoliasearch_instant/instant/show_suggestions_on_no_result_page';
42-
public const XML_ADD_TO_CART_ENABLE = 'algoliasearch_instant/instant/add_to_cart_enable';
43-
public const INFINITE_SCROLL_ENABLE = 'algoliasearch_instant/instant/infinite_scroll_enable';
44-
public const SEARCHBOX_ENABLE = 'algoliasearch_instant/instant/instantsearch_searchbox';
45-
public const HIDE_PAGINATION = 'algoliasearch_instant/instant/hide_pagination';
39+
public const FACETS = 'algoliasearch_instant/instant_facets/facets';
40+
public const MAX_VALUES_PER_FACET = 'algoliasearch_instant/instant_facets/max_values_per_facet';
41+
public const SORTING_INDICES = 'algoliasearch_instant/instant_sorts/sorts';
42+
public const SEARCHBOX_ENABLE = 'algoliasearch_instant/instant_options/instantsearch_searchbox';
43+
public const SHOW_SUGGESTIONS_NO_RESULTS = 'algoliasearch_instant/instant_options/show_suggestions_on_no_result_page';
44+
public const XML_ADD_TO_CART_ENABLE = 'algoliasearch_instant/instant_options/add_to_cart_enable';
45+
public const INFINITE_SCROLL_ENABLE = 'algoliasearch_instant/instant_options/infinite_scroll_enable';
46+
public const HIDE_PAGINATION = 'algoliasearch_instant/instant_options/hide_pagination';
4647

4748
public const IS_POPUP_ENABLED = 'algoliasearch_autocomplete/autocomplete/is_popup_enabled';
4849
public const NB_OF_PRODUCTS_SUGGESTIONS = 'algoliasearch_autocomplete/autocomplete/nb_of_products_suggestions';
@@ -135,7 +136,7 @@ class ConfigHelper
135136
protected const IS_ADDTOCART_ENABLED_IN_FREQUENTLY_BOUGHT_TOGETHER = 'algoliasearch_recommend/recommend/frequently_bought_together/is_addtocart_enabled';
136137
protected const IS_ADDTOCART_ENABLED_IN_RELATED_PRODUCTS = 'algoliasearch_recommend/recommend/related_product/is_addtocart_enabled';
137138
protected const IS_ADDTOCART_ENABLED_IN_TRENDS_ITEM = 'algoliasearch_recommend/recommend/trends_item/is_addtocart_enabled';
138-
protected const USE_VIRTUAL_REPLICA_ENABLED = 'algoliasearch_instant/instant/use_virtual_replica';
139+
protected const USE_VIRTUAL_REPLICA_ENABLED = 'algoliasearch_instant/instant/use_virtual_replica'; //legacy config
139140
protected const AUTOCOMPLETE_KEYBORAD_NAVIAGATION = 'algoliasearch_autocomplete/autocomplete/navigator';
140141
protected const FREQUENTLY_BOUGHT_TOGETHER_TITLE = 'algoliasearch_recommend/recommend/frequently_bought_together/title';
141142
protected const RELATED_PRODUCTS_TITLE = 'algoliasearch_recommend/recommend/related_product/title';
@@ -145,6 +146,10 @@ class ConfigHelper
145146
public const ENHANCED_QUEUE_ARCHIVE = 'algoliasearch_advanced/queue/enhanced_archive';
146147
public const NUMBER_OF_ELEMENT_BY_PAGE = 'algoliasearch_advanced/queue/number_of_element_by_page';
147148
public const ARCHIVE_LOG_CLEAR_LIMIT = 'algoliasearch_advanced/queue/archive_clear_limit';
149+
// https://www.algolia.com/doc/guides/managing-results/refine-results/sorting/in-depth/replicas/#what-are-virtual-replicas
150+
public const MAX_VIRTUAL_REPLICA_LIMIT = 20;
151+
152+
public const SORT_ATTRIBUTE_PRICE = 'price';
148153

149154
/**
150155
* @var Magento\Framework\App\Config\ScopeConfigInterface
@@ -206,6 +211,11 @@ class ConfigHelper
206211
*/
207212
protected $cookieHelper;
208213

214+
/**
215+
* @var array<int,<array<string, mixed>>>
216+
*/
217+
protected array $_sortingIndices = [];
218+
209219
/**
210220
* @param Magento\Framework\App\Config\ScopeConfigInterface $configInterface
211221
* @param StoreManagerInterface $storeManager
@@ -1005,17 +1015,107 @@ public function getAutocompleteMinimumCharacterLength($storeId = null): int
10051015
}
10061016

10071017
/**
1008-
* @param $originalIndexName
1009-
* @param $storeId
1010-
* @param $currentCustomerGroupId
1011-
* @param $attrs
1018+
* When group pricing is enabled a replica must be created for each possible sort
1019+
*
1020+
* @param string $originalIndexName
1021+
* @param int $customerGroupId
1022+
* @param string $currency
1023+
* @param array $origAttr
10121024
* @return array
1025+
*/
1026+
protected function getCustomerGroupSortPriceOverride(
1027+
string $originalIndexName,
1028+
int $customerGroupId,
1029+
string $currency,
1030+
array $origAttr): array
1031+
{
1032+
$attrName = $origAttr['attribute'];
1033+
$sortDir = $origAttr['sort'];
1034+
$groupIndexNameSuffix = 'group_' . $customerGroupId;
1035+
$groupIndexName =
1036+
$originalIndexName . '_' . $attrName . '_' . $groupIndexNameSuffix . '_' . $sortDir;
1037+
$groupSortAttribute = $attrName . '.' . $currency . '.' . $groupIndexNameSuffix;
1038+
$newAttr = [
1039+
'attribute' => $attrName,
1040+
'name' => $groupIndexName,
1041+
'sort' => $sortDir,
1042+
'sortLabel' => $origAttr['sortLabel']
1043+
];
1044+
1045+
$newAttr['ranking'] = $this->getSortAttributingRankingSetting($groupSortAttribute, $sortDir);
1046+
return $this->decorateSortAttribute($newAttr);
1047+
}
1048+
1049+
/*
1050+
* Add data to the sort attribute object
1051+
*/
1052+
protected function decorateSortAttribute(array $attr): array {
1053+
if (!array_key_exists('label', $attr) && array_key_exists('sortLabel', $attr)) {
1054+
$attr['label'] = $attr['sortLabel'];
1055+
}
1056+
return $attr;
1057+
}
1058+
1059+
/**
1060+
* Get ranking setting to be used for the standard sorting replica
1061+
* @param string $attrName
1062+
* @param string $sortDir
1063+
* @return string[]
1064+
*/
1065+
protected function getSortAttributingRankingSetting(string $attrName, string $sortDir): array
1066+
{
1067+
return [
1068+
$sortDir . '(' . $attrName . ')',
1069+
'typo',
1070+
'geo',
1071+
'words',
1072+
'filters',
1073+
'proximity',
1074+
'attribute',
1075+
'exact',
1076+
'custom',
1077+
];
1078+
}
1079+
1080+
/**
1081+
* @throws LocalizedException
1082+
*/
1083+
protected function isGroupPricingExcludedFromWebsite(int $customerGroupId, int $websiteId): bool
1084+
{
1085+
$excludedWebsites = $this->groupExcludedWebsiteRepository->getCustomerGroupExcludedWebsites($customerGroupId);
1086+
return in_array($websiteId, $excludedWebsites);
1087+
}
1088+
1089+
/**
1090+
* Augment sorting configuration with corresponding replica indices, ranking,
1091+
* and (as needed) customer group pricing
1092+
*
1093+
* @param string $originalIndexName
1094+
* @param ?int $storeId
1095+
* @param ?int $currentCustomerGroupId
1096+
* @param ?array $attrs - serialized array of sorting attributes to transform (defaults to saved sorting config)
1097+
* @return array of transformed sorting / replica objects
10131098
* @throws Magento\Framework\Exception\LocalizedException
10141099
* @throws Magento\Framework\Exception\NoSuchEntityException
10151100
*/
1016-
public function getSortingIndices($originalIndexName, $storeId = null, $currentCustomerGroupId = null, $attrs = null)
1101+
public function getSortingIndices(
1102+
string $originalIndexName,
1103+
int $storeId = null,
1104+
int $currentCustomerGroupId = null,
1105+
array $attrs = null
1106+
): array
10171107
{
1018-
if (!$attrs){
1108+
// Selectively cache this result - only cache manipulation of saved settings per store
1109+
$useCache = is_null($currentCustomerGroupId) && is_null($attrs);
1110+
1111+
if ($useCache
1112+
&& array_key_exists($storeId, $this->_sortingIndices)
1113+
&& is_array($this->_sortingIndices[$storeId])) {
1114+
return $this->_sortingIndices[$storeId];
1115+
}
1116+
1117+
// If no sorting configuration is supplied - obtain from the saved configuration
1118+
if (!$attrs) {
10191119
$attrs = $this->getSorting($storeId);
10201120
}
10211121

@@ -1024,85 +1124,54 @@ public function getSortingIndices($originalIndexName, $storeId = null, $currentC
10241124
foreach ($attrs as $key => $attr) {
10251125
$indexName = false;
10261126
$sortAttribute = false;
1027-
if ($this->isCustomerGroupsEnabled($storeId) && $attr['attribute'] === 'price') {
1028-
$websiteId = (int)$this->storeManager->getStore($storeId)->getWebsiteId();
1127+
// Group pricing
1128+
if ($this->isCustomerGroupsEnabled($storeId) && $attr['attribute'] === self::SORT_ATTRIBUTE_PRICE) {
1129+
$websiteId = (int) $this->storeManager->getStore($storeId)->getWebsiteId();
10291130
$groupCollection = $this->groupCollection;
10301131
if (!is_null($currentCustomerGroupId)) {
10311132
$groupCollection->addFilter('customer_group_id', $currentCustomerGroupId);
10321133
}
10331134
foreach ($groupCollection as $group) {
1034-
$customerGroupId = (int)$group->getData('customer_group_id');
1035-
$excludedWebsites = $this->groupExcludedWebsiteRepository->getCustomerGroupExcludedWebsites($customerGroupId);
1036-
if (in_array($websiteId, $excludedWebsites)) {
1037-
continue;
1135+
$customerGroupId = (int) $group->getData('customer_group_id');
1136+
if (!$this->isGroupPricingExcludedFromWebsite($customerGroupId, $websiteId)) {
1137+
$newAttr = $this->getCustomerGroupSortPriceOverride($originalIndexName, $customerGroupId, $currency, $attr);;
1138+
$attributesToAdd[$newAttr['sort']][] = $this->decorateSortAttribute($newAttr);
10381139
}
1039-
$groupIndexNameSuffix = 'group_' . $customerGroupId;
1040-
$groupIndexName =
1041-
$originalIndexName . '_' . $attr['attribute'] . '_' . $groupIndexNameSuffix . '_' . $attr['sort'];
1042-
$groupSortAttribute = $attr['attribute'] . '.' . $currency . '.' . $groupIndexNameSuffix;
1043-
$newAttr = [];
1044-
$newAttr['name'] = $groupIndexName;
1045-
$newAttr['attribute'] = $attr['attribute'];
1046-
$newAttr['sort'] = $attr['sort'];
1047-
$newAttr['sortLabel'] = $attr['sortLabel'];
1048-
if (!array_key_exists('label', $newAttr) && array_key_exists('sortLabel', $newAttr)) {
1049-
$newAttr['label'] = $newAttr['sortLabel'];
1050-
}
1051-
$newAttr['ranking'] = [
1052-
$newAttr['sort'] . '(' . $groupSortAttribute . ')',
1053-
'typo',
1054-
'geo',
1055-
'words',
1056-
'filters',
1057-
'proximity',
1058-
'attribute',
1059-
'exact',
1060-
'custom',
1061-
];
1062-
$attributesToAdd[$newAttr['sort']][] = $newAttr;
10631140
}
1064-
} elseif ($attr['attribute'] === 'price') {
1141+
// Regular pricing
1142+
} elseif ($attr['attribute'] === self::SORT_ATTRIBUTE_PRICE) {
10651143
$indexName = $originalIndexName . '_' . $attr['attribute'] . '_' . 'default' . '_' . $attr['sort'];
10661144
$sortAttribute = $attr['attribute'] . '.' . $currency . '.' . 'default';
1145+
// All other sort attributes
10671146
} else {
10681147
$indexName = $originalIndexName . '_' . $attr['attribute'] . '_' . $attr['sort'];
10691148
$sortAttribute = $attr['attribute'];
10701149
}
1150+
1151+
// Decorate all non group pricing attributes
10711152
if ($indexName && $sortAttribute) {
10721153
$attrs[$key]['name'] = $indexName;
1073-
if (!array_key_exists('label', $attrs[$key]) && array_key_exists('sortLabel', $attrs[$key])) {
1074-
$attrs[$key]['label'] = $attrs[$key]['sortLabel'];
1075-
}
1076-
$attrs[$key]['ranking'] = [
1077-
$attr['sort'] . '(' . $sortAttribute . ')',
1078-
'typo',
1079-
'geo',
1080-
'words',
1081-
'filters',
1082-
'proximity',
1083-
'attribute',
1084-
'exact',
1085-
'custom',
1086-
];
1154+
$attrs[$key]['ranking'] = $this->getSortAttributingRankingSetting($sortAttribute, $attr['sort']);
1155+
$attrs[$key] = $this->decorateSortAttribute($attrs[$key]);
10871156
}
10881157
}
10891158
$attrsToReturn = [];
1090-
if (count($attributesToAdd) > 0) {
1091-
foreach ($attrs as $key => $attr) {
1092-
if ($attr['attribute'] == 'price' && isset($attributesToAdd[$attr['sort']])) {
1093-
$attrsToReturn = array_merge($attrsToReturn, $attributesToAdd[$attr['sort']]);
1094-
} else {
1095-
$attrsToReturn[] = $attr;
1096-
}
1159+
1160+
foreach ($attrs as $attr) {
1161+
if ($attr['attribute'] == self::SORT_ATTRIBUTE_PRICE
1162+
&& count($attributesToAdd)
1163+
&& isset($attributesToAdd[$attr['sort']])) {
1164+
$attrsToReturn = array_merge($attrsToReturn, $attributesToAdd[$attr['sort']]);
1165+
} else {
1166+
$attrsToReturn[] = $attr;
10971167
}
10981168
}
1099-
if (count($attrsToReturn) > 0) {
1100-
return $attrsToReturn;
1101-
}
1102-
if (is_array($attrs)) {
1103-
return $attrs;
1169+
1170+
if ($useCache) {
1171+
$this->_sortingIndices[$storeId] = $attrsToReturn;
11041172
}
1105-
return [];
1173+
1174+
return $attrsToReturn;
11061175
}
11071176

11081177
/***
@@ -1791,14 +1860,17 @@ public function getCacheTime($storeId = null)
17911860
}
17921861

17931862
/**
1794-
* @param $storeId
1795-
* @return mixed
1863+
* @param int|null $storeId
1864+
* @return bool
17961865
*/
1797-
public function useVirtualReplica($storeId = null) {
1798-
return $this->configInterface->isSetFlag(self::USE_VIRTUAL_REPLICA_ENABLED,
1799-
ScopeInterface::SCOPE_STORE,
1800-
$storeId
1801-
);
1866+
public function useVirtualReplica(?int $storeId = null): bool
1867+
{
1868+
return (bool) count(array_filter(
1869+
$this->getSorting($storeId),
1870+
function ($sort) {
1871+
return $sort['virtualReplica'];
1872+
}
1873+
));
18021874
}
18031875

18041876
/**

0 commit comments

Comments
 (0)