Skip to content

Commit 2a214ce

Browse files
authored
Merge pull request #7 from develodesign/feature/AddTypeSenseAdapter
Add TypeSenses' JS Adapter to Algolia instantsearch
2 parents e3cfb42 + de5882d commit 2a214ce

29 files changed

+1553
-76
lines changed

Adapter/Client.php

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use Typesense\Client as TypeSenseClient;
66
use Develo\Typesense\Services\ConfigService;
7+
use Algolia\AlgoliaSearch\Helper\Data as AlgoliaHelper;
78

89
/**
910
* Class Client
@@ -25,17 +26,25 @@ class Client
2526
*/
2627
private ?TypeSenseClient $typeSenseClient = null;
2728

29+
/**
30+
* $var AlgoliaHelper
31+
*/
32+
private $algoliaHelper;
33+
2834
/**
2935
* Initialise Typesense Client with Magento config
3036
*
3137
* @param ConfigService $configService
38+
* @param AlgoliaHelper $algoliaHelper
3239
* @throws \Typesense\Exceptions\ConfigError
3340
*/
3441
public function __construct(
3542
ConfigService $configService,
43+
AlgoliaHelper $algoliaHelper
3644
)
3745
{
3846
$this->configService = $configService;
47+
$this->algoliaHelper = $algoliaHelper;
3948
}
4049

4150
/**
@@ -54,9 +63,57 @@ public function deleteIndex(string $indexName): array
5463
*/
5564
public function addData($indexName, $data)
5665
{
66+
foreach ($data as &$item) {
67+
$item['id'] = (string)$item['objectID'];
68+
$item['objectID'] = (string)$item['objectID'];
69+
70+
71+
if (!isset($item['price']) || !isset($item['sku'])) {
72+
continue;
73+
}
74+
75+
if (is_string($item['sku'])) {
76+
$item['sku'] = [$item['sku']];
77+
}
78+
79+
foreach ($item['price'] as $currency => &$price) {
80+
81+
$price['special_from_date'] = (string)($price['special_from_date'] ?? '');
82+
$price['special_to_date'] = (string)($price['special_to_date'] ?? '');
83+
84+
$price['default'] = number_format($price['default'], 2);
85+
}
86+
}
87+
$indexName = rtrim($indexName, "_tmp");
5788
return $this->getTypesenseClient()->collections[$indexName]->getDocuments()->import($data, ['action' => 'upsert']);
5889
}
5990

91+
/**
92+
* @inheirtDoc
93+
*/
94+
public function deleteData($indexName, $data)
95+
{
96+
$searchParameters = [
97+
'q' => implode(",", $data),
98+
'query_by' => 'objectID',
99+
];
100+
101+
return $this->getTypesenseClient()->collections[$indexName]->documents->delete($searchParameters);
102+
}
103+
104+
/**
105+
* @inheirtDoc
106+
*/
107+
public function getData($indexName, $data)
108+
{
109+
$searchParameters = [
110+
'q' => implode(",", $data),
111+
'query_by' => 'objectID',
112+
];
113+
return ["results" => $this->getTypesenseClient()->collections[$indexName]->documents->search($searchParameters)];
114+
}
115+
116+
60117
/**
61118
* @return TypeSenseClient
62119
*/
@@ -82,6 +139,7 @@ public function getTypesenseClient(): TypeSenseClient
82139
}
83140
return $this->typeSenseClient;
84141
}
142+
85143
}
86144

87145

Helper/ConfigChangeHelper.php

Lines changed: 290 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,290 @@
1+
<?php
2+
3+
namespace Develo\Typesense\Helper;
4+
5+
use Algolia\AlgoliaSearch\Helper\Data as AlgoliaHelper;
6+
use Algolia\AlgoliaSearch\Helper\ConfigHelper as AlgoliaConfigHelper;
7+
use Develo\Typesense\Adapter\Client;
8+
use Develo\Typesense\Services\ConfigService;
9+
use Magento\Catalog\Api\Data\CategoryAttributeInterface;
10+
use Magento\Catalog\Api\Data\ProductAttributeInterface;
11+
use Magento\Eav\Api\AttributeRepositoryInterface;
12+
use Magento\Framework\Api\SearchCriteriaBuilder;
13+
use Magento\Framework\Api\SearchCriteriaBuilderFactory;
14+
use Magento\Framework\App\RequestInterface;
15+
use Magento\Store\Model\StoreManagerInterface;
16+
use Typesense\Client as TypeSenseClient;
17+
18+
class ConfigChangeHelper
19+
{
20+
21+
const INDEX_PRODUCTS = 'products';
22+
const INDEX_CATEGORIES = 'categories';
23+
const INDEX_PAGES = 'pages';
24+
25+
const REQUIRED_INDEXES = [
26+
self::INDEX_PRODUCTS,
27+
self::INDEX_CATEGORIES,
28+
self::INDEX_PAGES
29+
];
30+
31+
const SORTABLE_ATTRIBUTES = ['float', 'int64', 'string'];
32+
33+
/**
34+
* @var RequestInterface
35+
*/
36+
private $request;
37+
38+
/**
39+
* @var Client
40+
*/
41+
private $typesenseClient;
42+
43+
/**
44+
* @var AlgoliaHelper
45+
*/
46+
private $algoliaHelper;
47+
48+
/**
49+
* @var TypeSenseClient
50+
*/
51+
private $typeSenseCollecitons;
52+
53+
/**
54+
* @var StoreManagerInterface
55+
*/
56+
private $storeManager;
57+
58+
/**
59+
* @var ConfigService
60+
*/
61+
private ConfigService $configService;
62+
63+
/**
64+
* @var AlgoliaConfigHelper
65+
*/
66+
private AlgoliaConfigHelper $algoliaConfigHelper;
67+
68+
/**
69+
* @var AttributeRepositoryInterface
70+
*/
71+
private AttributeRepositoryInterface $attributeRepository;
72+
73+
74+
/**
75+
* @var SearchCriteriaBuilderFactory
76+
*/
77+
private SearchCriteriaBuilderFactory $searchCriteriaBuilderFactory;
78+
79+
public function __construct(
80+
RequestInterface $request,
81+
Client $client,
82+
AlgoliaHelper $algoliaHelper,
83+
StoreManagerInterface $storeManager,
84+
ConfigService $configService,
85+
AlgoliaConfigHelper $algoliaConfigHelper,
86+
AttributeRepositoryInterface $attributeRepository,
87+
SearchCriteriaBuilderFactory $searchCriteriaBuilderFactory
88+
)
89+
{
90+
$this->request = $request;
91+
$this->typesenseClient = $client->getTypesenseClient();
92+
$this->algoliaHelper = $algoliaHelper;
93+
$this->typeSenseCollecitons = $this->typesenseClient->collections;
94+
$this->storeManager = $storeManager;
95+
$this->configService = $configService;
96+
$this->algoliaConfigHelper = $algoliaConfigHelper;
97+
$this->attributeRepository = $attributeRepository;
98+
$this->searchCriteriaBuilderFactory = $searchCriteriaBuilderFactory;
99+
}
100+
101+
/**
102+
* Creates Indexes in Typesense after credentials have been updated
103+
*/
104+
public function setCollectionConfig()
105+
{
106+
107+
$facets = [];
108+
109+
foreach ($this->algoliaConfigHelper->getFacets() as $facet) {
110+
$facets[] = $facet['attribute'];
111+
}
112+
113+
$sortingAttributes = [];
114+
115+
foreach ($this->algoliaConfigHelper->getSorting() as $sorting) {
116+
$sortingAttributes[] = $sorting['attribute'];
117+
}
118+
119+
$indexes = $this->getMagentoIndexes();
120+
121+
$existingCollections = $this->getExistingCollections();
122+
123+
foreach ($indexes as $index) {
124+
125+
foreach (static::REQUIRED_INDEXES as $indexToCreate) {
126+
127+
$fields = $this->getFields($facets, $sortingAttributes, $indexToCreate);
128+
129+
$indexName = $index["indexName"] . "_{$indexToCreate}";
130+
131+
if (!isset($existingCollections[$indexName])) {
132+
133+
$this->typeSenseCollecitons->create(
134+
[
135+
'name' => $indexName,
136+
'enable_nested_fields' => true,
137+
'fields' => $fields
138+
]
139+
);
140+
141+
continue;
142+
}
143+
}
144+
}
145+
146+
return $this;
147+
}
148+
149+
/**
150+
* Gets existing collections from typesense
151+
*/
152+
private function getExistingCollections()
153+
{
154+
$collections = $this->typeSenseCollecitons->retrieve();
155+
$existingCollections = [];
156+
foreach ($collections as $collection) {
157+
$existingCollections[$collection["name"]] = $collection;
158+
}
159+
return $existingCollections;
160+
}
161+
162+
/**
163+
* Gets an Aloliga index name for each store
164+
*/
165+
private function getMagentoIndexes()
166+
{
167+
$indexNames = [];
168+
foreach ($this->storeManager->getStores() as $store) {
169+
$indexNames[$store->getId()] = [
170+
'indexName' => $this->algoliaHelper->getBaseIndexName($store->getId()),
171+
'priceKey' => '.' . $store->getCurrentCurrencyCode($store->getId()) . '.default',
172+
];
173+
}
174+
return $indexNames;
175+
}
176+
177+
public function getFields(array $facets, array $sortingAttributes, string $index) : array {
178+
switch ($index) {
179+
case 'products':
180+
$attributes = $this->algoliaConfigHelper->getProductAdditionalAttributes();
181+
$entityTypeCode = ProductAttributeInterface::ENTITY_TYPE_CODE;
182+
$defaultAttributes = [
183+
['name' => 'objectID', 'type' => 'string', 'facet' => true],
184+
['name' => 'categories', 'type' => 'object', 'facet' => true],
185+
['name' => 'visibility_search', 'type' => 'int64'],
186+
['name' => 'visibility_catalog', 'type' => 'int64', 'facet' => true]
187+
];
188+
189+
break;
190+
case 'categories':
191+
$attributes = $this->algoliaConfigHelper->getCategoryAdditionalAttributes();
192+
$entityTypeCode = CategoryAttributeInterface::ENTITY_TYPE_CODE;
193+
$defaultAttributes = [
194+
['name' => 'objectID', 'type' => 'string', 'facet' => true],
195+
];
196+
break;
197+
case 'pages':
198+
default:
199+
return [
200+
['name' => 'objectID', 'type' => 'string'],
201+
['name' => 'content', 'type' => 'string'],
202+
['name' => 'slug', 'type' => 'string'],
203+
['name' => 'name', 'type' => 'string']
204+
];
205+
}
206+
207+
$attributeCodes = [];
208+
foreach ($attributes as $attribute) {
209+
if ($attribute['searchable'] === '1' || in_array($attribute['attribute'], $facets)) {
210+
$attributeCodes[] = $attribute['attribute'];
211+
}
212+
}
213+
214+
/** @var SearchCriteriaBuilder $searchCriteria */
215+
$searchCriteria = $this->searchCriteriaBuilderFactory->create();
216+
$searchCriteria->addFilter('attribute_code', $attributeCodes, 'IN');
217+
218+
$attributeCollection = $this->attributeRepository->getList($entityTypeCode, $searchCriteria->create());
219+
220+
$backendTypes = [
221+
'datetime' => 'string',
222+
'decimal' => 'float',
223+
'int' => 'int64',
224+
'static' => 'string',
225+
'text' => 'string',
226+
'varchar' => 'string'
227+
];
228+
229+
$fields = [];
230+
foreach ($attributeCollection->getItems() as $attribute) {
231+
if (!isset($backendTypes[$attribute->getBackendType()]) || !$attribute->getIsRequired()) {
232+
continue;
233+
}
234+
235+
if ($attribute->getAttributeCode() === 'price') {
236+
$fields[] = [
237+
'name' => $attribute->getAttributeCode(),
238+
'type' => 'object'
239+
];
240+
241+
$fields[] = [
242+
'name' => 'price_default',
243+
'type' => 'float',
244+
'sort' => true
245+
];
246+
247+
continue;
248+
}
249+
250+
if ($attribute->getAttributeCode() === 'sku') {
251+
$fields[] = [
252+
'name' => $attribute->getAttributeCode(),
253+
'type' => 'string[]',
254+
'facet' => in_array($attribute->getAttributeCode(), $facets),
255+
'sort' => in_array($attribute->getAttributeCode(), $sortingAttributes),
256+
];
257+
258+
continue;
259+
}
260+
261+
$fields[] = [
262+
'name' => $attribute->getAttributeCode(),
263+
'type' => $backendTypes[$attribute->getBackendType()],
264+
'facet' => in_array($attribute->getAttributeCode(), $facets),
265+
'sort' => in_array($attribute->getAttributeCode(), $sortingAttributes) &&
266+
in_array($backendTypes[$attribute->getBackendType()], self::SORTABLE_ATTRIBUTES),
267+
];
268+
}
269+
270+
$fields = array_merge($fields, $defaultAttributes);
271+
272+
$fields = array_unique($fields, SORT_REGULAR);
273+
274+
return array_values($fields);
275+
}
276+
277+
public function getSearchableAttributes(string $index = self::INDEX_PRODUCTS) : string
278+
{
279+
$attributes = [];
280+
foreach ($this->getFields([], [], $index) as $field) {
281+
if (!in_array($field['type'], ['string', 'string[]'])) {
282+
continue;
283+
}
284+
285+
$attributes[] = $field['name'];
286+
}
287+
288+
return implode(',', $attributes);
289+
}
290+
}

0 commit comments

Comments
 (0)