Skip to content

Commit 399c7df

Browse files
authored
Merge pull request #10 from develodesign/feature/searchable-attributes-and-autocomplete
Fix autocomplete and searchable attributes
2 parents 39f9c6d + f0fc00f commit 399c7df

File tree

8 files changed

+279
-40
lines changed

8 files changed

+279
-40
lines changed

Adapter/Client.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ public function deleteIndex(string $indexName): array
6464
*/
6565
public function addData($indexName, $data)
6666
{
67-
$facets = [];
67+
$facets = $this->getFacets();
6868
foreach ($data as &$item) {
6969
$item['id'] = (string)$item['objectID'];
7070
$item['objectID'] = (string)$item['objectID'];

Helper/ConfigChangeHelper.php

Lines changed: 36 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace Develo\Typesense\Helper;
44

5+
use Algolia\AlgoliaSearch\Helper\ConfigHelper;
56
use Algolia\AlgoliaSearch\Helper\Data as AlgoliaHelper;
67
use Algolia\AlgoliaSearch\Helper\ConfigHelper as AlgoliaConfigHelper;
78
use Develo\Typesense\Adapter\Client;
@@ -136,7 +137,6 @@ public function setCollectionConfig()
136137
unset($existingCollections[$indexName]);
137138
}
138139

139-
140140
$this->typeSenseCollecitons->create(
141141
[
142142
'name' => $indexName,
@@ -189,7 +189,20 @@ public function getFields(array $facets, array $sortingAttributes, string $index
189189
['name' => 'objectID', 'type' => 'string', 'facet' => true],
190190
['name' => 'categories', 'type' => 'object', 'facet' => true],
191191
['name' => 'visibility_search', 'type' => 'int64'],
192-
['name' => 'visibility_catalog', 'type' => 'int64', 'facet' => true]
192+
['name' => 'visibility_catalog', 'type' => 'int64', 'facet' => true],
193+
[
194+
'name' => 'price_default',
195+
'type' => 'float',
196+
'sort' => true,
197+
'facet' => true
198+
],
199+
200+
[
201+
'name' => 'sku',
202+
'type' => 'string[]',
203+
'facet' => in_array('sku', $facets),
204+
'sort' => in_array('sku', $sortingAttributes)
205+
]
193206
];
194207

195208
// The hierarchal menu widget expects 10 levels of category.
@@ -222,7 +235,7 @@ public function getFields(array $facets, array $sortingAttributes, string $index
222235

223236
$attributeCodes = [];
224237
foreach ($attributes as $attribute) {
225-
if ($attribute['searchable'] === '1' || in_array($attribute['attribute'], $facets)) {
238+
if ($attribute['searchable'] === '1') {
226239
$attributeCodes[] = $attribute['attribute'];
227240
}
228241
}
@@ -234,44 +247,19 @@ public function getFields(array $facets, array $sortingAttributes, string $index
234247
$attributeCollection = $this->attributeRepository->getList($entityTypeCode, $searchCriteria->create());
235248

236249
$fields = [];
237-
foreach ($attributeCollection->getItems() as $attribute) {
238-
if ($attribute->getAttributeCode() === 'price') {
239-
$fields[] = [
240-
'name' => $attribute->getAttributeCode(),
241-
'type' => 'object'
242-
];
243-
244-
$fields[] = [
245-
'name' => 'price_default',
246-
'type' => 'float',
247-
'sort' => true
248-
];
249-
250-
continue;
251-
}
252-
253-
if ($attribute->getAttributeCode() === 'sku') {
254-
$fields[] = [
255-
'name' => $attribute->getAttributeCode(),
256-
'type' => 'string[]',
257-
'facet' => in_array($attribute->getAttributeCode(), $facets),
258-
'sort' => in_array($attribute->getAttributeCode(), $sortingAttributes),
259-
];
260250

251+
foreach ($attributeCollection->getItems() as $attribute) {
252+
if (in_array($attribute->getAttributeCode(), ['price', 'sku'])) {
261253
continue;
262254
}
263255

264256
$isFacet = in_array($attribute->getAttributeCode(), $facets);
265257

266-
if (!$isFacet) {
267-
continue;
268-
}
269-
270258
$fields[] = [
271259
'name' => $attribute->getAttributeCode(),
272-
'type' => 'string[]',
260+
'type' => $isFacet ? 'string[]' : 'string',
273261
'facet' => $isFacet,
274-
'sort' => false,
262+
'sort' => in_array($attribute->getAttributeCode(), $sortingAttributes),
275263
'optional' => !$attribute->getIsRequired()
276264
];
277265
}
@@ -286,14 +274,24 @@ public function getFields(array $facets, array $sortingAttributes, string $index
286274
public function getSearchableAttributes(string $index = self::INDEX_PRODUCTS): string
287275
{
288276
$attributes = [];
289-
foreach ($this->getFields([], [], $index) as $field) {
290-
if (!in_array($field['type'], ['string', 'string[]'])) {
291-
continue;
292-
}
277+
switch ($index) {
278+
case 'products':
279+
$attributes = $this->algoliaConfigHelper->getProductAdditionalAttributes();
280+
break;
281+
case 'categories':
282+
$attributes = $this->algoliaConfigHelper->getCategoryAdditionalAttributes();
283+
break;
284+
case 'pages':
285+
return 'name,slug';
286+
}
293287

294-
$attributes[] = $field['name'];
288+
$searchableAttributes = [];
289+
foreach ($attributes as $attribute) {
290+
if ($attribute['searchable'] === '1') {
291+
$searchableAttributes[] = $attribute['attribute'];
292+
}
295293
}
296294

297-
return implode(',', $attributes);
295+
return implode(',', $searchableAttributes);
298296
}
299297
}

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,20 @@ For more information on customizing the Algolia module, please refer to the foll
5353
- [Customizing Instant Search Page](https://www.algolia.com/doc/integration/magento-2/customize/instant-search-page/)
5454
- [Customizing Custom Front-end Events](https://www.algolia.com/doc/integration/magento-2/customize/custom-front-end-events/)
5555

56+
When migrating from Algolia, you will need to remove "Price" from the facets and review the Product and Category searchable attributes. Typesense is much more strict when querying so if an attribute does not exist it will throw an error.
57+
58+
Review the following config and set searchable to "No" when applicable:
59+
60+
Settings > Algolia > Products > Attributes
61+
62+
## Debugging config
63+
64+
You may get errors such as:
65+
66+
`pesense-adapter.js:1 Uncaught (in promise) Error: 404 - Could not find a field named "path" in the schema.`
67+
68+
This is because you either have a searchable attribute for products which does not exist, or perhaps a facet attribute which does not exist. You should remove the attribute from these areas and try again.
69+
5670
## Documentation
5771

5872
For more information about Typesense, check out their [official documentation](https://typesense.org/docs/).

view/frontend/layout/default.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,10 @@
44
<head>
55
<script src="Develo_Typesense::js/magento-adapter.js"/>
66
</head>
7+
8+
<referenceBlock name="algolia.autocomplete.page">
9+
<action method="setTemplate">
10+
<argument name="template" xsi:type="string">Develo_Typesense::autocomplete/page.phtml</argument>
11+
</action>
12+
</referenceBlock>
713
</page>

view/frontend/requirejs-config.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
var config = {
2+
map: {
3+
'*': {
4+
'autocomplete': 'Develo_Typesense/js/autocomplete'
5+
}
6+
}
7+
};
Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
requirejs([
2+
'algoliaBundle',
3+
'Develo_Typesense/js/typesense.min',
4+
'domReady!'
5+
], function (algoliaBundle, Typesense) {
6+
algoliaBundle.$(function ($) {
7+
8+
const {autocomplete} = algoliaBundle;
9+
const {resultURL} = algoliaConfig;
10+
11+
if (algoliaConfig.autocomplete.nbOfProductsSuggestions > 0) {
12+
algoliaConfig.autocomplete.sections.unshift({
13+
hitsPerPage: algoliaConfig.autocomplete.nbOfProductsSuggestions,
14+
label: algoliaConfig.translations.products,
15+
name: "products"
16+
});
17+
}
18+
19+
if (algoliaConfig.autocomplete.nbOfCategoriesSuggestions > 0) {
20+
algoliaConfig.autocomplete.sections.unshift({
21+
hitsPerPage: algoliaConfig.autocomplete.nbOfCategoriesSuggestions,
22+
label: algoliaConfig.translations.categories,
23+
name: "categories"
24+
});
25+
}
26+
27+
if (algoliaConfig.autocomplete.nbOfQueriesSuggestions > 0) {
28+
algoliaConfig.autocomplete.sections.unshift({
29+
hitsPerPage: algoliaConfig.autocomplete.nbOfQueriesSuggestions,
30+
label: '',
31+
name: "suggestions"
32+
});
33+
}
34+
35+
algoliaConfig.autocomplete.templates = {
36+
products: algoliaBundle.Hogan.compile($('#autocomplete_products_template').html()),
37+
categories: algoliaBundle.Hogan.compile($('#autocomplete_categories_template').html()),
38+
pages: algoliaBundle.Hogan.compile($('#autocomplete_pages_template').html())
39+
};
40+
41+
const getQueryBy = function (name) {
42+
if (
43+
typeof algoliaConfig.typesense_searchable !== 'undefined' &&
44+
typeof algoliaConfig.typesense_searchable[name] !== 'undefined'
45+
) {
46+
return algoliaConfig.typesense_searchable[name];
47+
}
48+
49+
return 'name'
50+
}
51+
52+
// taken from common.js (autocomplete v0) and adopted to autocomplete v1
53+
const getAutocompleteSource = function ({section, setContext}) {
54+
if (section.hitsPerPage <= 0)
55+
return null;
56+
57+
var options = {
58+
hitsPerPage: section.hitsPerPage,
59+
analyticsTags: 'autocomplete',
60+
clickAnalytics: true
61+
};
62+
63+
var source = {};
64+
65+
var templates = {};
66+
67+
switch (section.name) {
68+
case 'products':
69+
options.numericFilters = 'visibility_search=1';
70+
options.ruleContexts = ['magento_filters', '']; // Empty context to keep BC for already create rules in dashboard
71+
break;
72+
case 'categories':
73+
if (algoliaConfig.showCatsNotIncludedInNavigation === false) {
74+
options.numericFilters = 'include_in_menu=1';
75+
}
76+
break;
77+
}
78+
79+
templates = {
80+
header({html}) {
81+
return html`<h3>${section.label}</h3>`;
82+
},
83+
item({item, html}) {
84+
const innerHtml = algoliaConfig.autocomplete.templates[section.name].render(item);
85+
86+
return html`<div dangerouslySetInnerHTML=${{ __html: innerHtml }}></div>`
87+
}
88+
}
89+
90+
source = {
91+
...options,
92+
indexName: algoliaConfig.indexName + "_" + section.name,
93+
name: section.name,
94+
templates
95+
};
96+
97+
return source;
98+
};
99+
100+
const plugins = []
101+
102+
if (window.initExtraAlgoliaConfiguration) {
103+
const {plugins: extraPlugins} = initExtraAlgoliaConfiguration(algoliaConfig)
104+
plugins.push(...extraPlugins)
105+
}
106+
107+
autocomplete({
108+
container: '#algoliaAutocomplete',
109+
placeholder: algoliaConfig.translations.placeholder,
110+
debug: algoliaConfig.autocomplete.isDebugEnabled,
111+
plugins,
112+
detachedMediaQuery: 'none',
113+
onSubmit: (params) => {
114+
window.location.href = `${resultURL}?q=${params.state.query}`
115+
},
116+
classNames: {
117+
list: 'w-full flex flex-wrap py-4 px-2',
118+
item: 'w-full lg:w-1/2 p-2 hover:bg-gray-200',
119+
sourceHeader: 'px-2 py-4 uppercase tracking-widest text-blue-500',
120+
source: 'flex flex-col',
121+
panel: 'mx-4 absolute w-full bg-white z-50 border border-gray-300',
122+
input: 'w-full p-2 text-base lg:text-lg leading-7 tracking-wider border border-gray-300',
123+
form: 'w-full relative flex items-center',
124+
inputWrapper: 'flex-grow px-4',
125+
inputWrapperPrefix: 'hidden',
126+
inputWrapperSuffix: 'hidden',
127+
label: 'm-0 leading-none',
128+
submitButton: 'leading-none'
129+
},
130+
async getSources({query, setContext}) {
131+
/** Setup autocomplete data sources **/
132+
var sources = [];
133+
for (let i = 0; i < algoliaConfig.autocomplete.sections.length; i++) {
134+
135+
let section = algoliaConfig.autocomplete.sections[i];
136+
137+
var source = getAutocompleteSource({section, setContext});
138+
139+
// autocomplete v1 adapter
140+
if (source) {
141+
142+
let typesenseClient = new Typesense.Client(algoliaConfig.typesense.config)
143+
144+
const results = await typesenseClient.collections(source.indexName).documents().search({
145+
q: query,
146+
query_by: getQueryBy(source.name),
147+
per_page: source.hitsPerPage
148+
})
149+
150+
sources.push({
151+
sourceId: source.name,
152+
query,
153+
getItems() {
154+
return results.hits.map(hit => (
155+
hit.document
156+
));
157+
},
158+
templates: source.templates
159+
});
160+
}
161+
}
162+
163+
return sources;
164+
},
165+
166+
render({elements, render, html}, root) {
167+
const {categories, pages, products} = elements;
168+
169+
render(
170+
html`<div class="relative w-full flex flex-col lg:flex-row">
171+
<div class="w-full lg:order-1 lg:border-l-2">${products}</div>
172+
<div class="w-full px-1 pb-10 md:px-2">${categories} ${pages}</div>
173+
</div>`,
174+
root
175+
);
176+
},
177+
178+
renderNoResults({state, render, html}, root) {
179+
const suggestions = [];
180+
181+
if (algoliaConfig.showSuggestionsOnNoResultsPage && algoliaConfig.popularQueries.length > 0) {
182+
algoliaConfig.popularQueries
183+
.slice(0, Math.min(3, algoliaConfig.popularQueries.length))
184+
.forEach(function (query) {
185+
suggestions.push({
186+
url: algoliaConfig.baseUrl + '/catalogsearch/result/?q=' + encodeURIComponent(query),
187+
query
188+
});
189+
});
190+
}
191+
192+
render(html`
193+
<div class="p-4 lg:p-6">
194+
<div class="lx:mb-2">
195+
<span class="pr-1 text-base xl:text-lg font-bold tracking-wide">${algoliaConfig.translations.noProducts}</span>
196+
<span class="text-base font-bold tracking-wide">"${state.query}"</span>
197+
</div>
198+
<div class="see-all">
199+
${(algoliaConfig.showSuggestionsOnNoResultsPage && suggestions.length > 0 ?
200+
html`<div class="py-4 lg:py-6">
201+
<span class="pr-1 text-sm xl:text-base font-bold tracking-wider">${algoliaConfig.translations.popularQueries}</span>
202+
${suggestions.map(({url, query}) => html`<a class="text-sm xl:text-base text-gray-600 tracking-wide font-semibold hover:underline" href="${url}">${query}</a>`)}
203+
</div>` : '')
204+
}
205+
<a class="py-2 text-sm xl:text-base text-gray-600 tracking-wide font-bold hover:underline" href="${algoliaConfig.baseUrl}/catalogsearch/result/?q=__empty__">${algoliaConfig.translations.seeAll}</a>
206+
</div>
207+
</div>`, root);
208+
},
209+
}
210+
);
211+
})
212+
});

view/frontend/web/js/internals/autocompleteConfig.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ const initAutoComplete = () => {
103103

104104
autocomplete({
105105
container: '#algolia-autocomplete-container',
106-
placeholder: algoliaConfig.placeholder,
106+
placeholder: algoliaConfig.translations.placeholder,
107107
debug: algoliaConfig.autocomplete.isDebugEnabled,
108108
plugins,
109109
detachedMediaQuery: 'none',

view/frontend/web/js/typesense.min.js

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)