Na projekte conrad.hu sme po spustení eshopu používali na vyhľadávanie produktov vlastné riešenie využívajúce Elasicsearch hostovaný na našom vlastnom serveri. S týmto vyhľadávaním sme ale neboli celkom spokojný a to z nasledujúcich dôvodov:

  • Zákazník mal dosť špecifické požiadavky na zoraďovanie výsledkov a váhu jednotlivých polí v indexe. Taktiež chcel mať možnosť upravovať nastavenia cez administráciu.
  • Zákazník chcel mať podporu maďarského jazyka, automatickú opravu chýb ako aj možnosť importovať a následne editovať synonymá cez administračné rozhranie.
  • Produktový katalóg obsahuje zhruba 500 000 produktov, pričom každý produkt môže obsahovať až desiatky parametrov. Fazetové vyhľadávanie pri tomto množstve parametrov bolo v prípade Elasticsearch dosť pomalé a nedostačujúce.

Preto sme sa rozhodli použiť iný vyhľadávací engine. Po dôkladnom porovnaní viacerých možností padla voľba na vyhľadávač Algolia. Algolia má prehľadnú administráciu, možnosť skúsiť trial verziu na 14 dní zadarmo a tiež obsiahlu a prehľadnú dokumentáciu, ktorá podstatne urýchli implementáciu.

Vytvorenie a naplnenie indexu

Na začiatku je potrebné vytvoriť a naplniť index dátami (produktami). Index je možné vytvoriť v administračnom rozhraní Algolia cez menu Indices -> New. Názov indexu by mal obsahovať prefix prod prípadne test, podľa toho či sa jedná o testovaciu alebo produkčnú databázu. Náš produkčný index sa teda volá prod_products. Naplnenie dátami je možné spraviť viacerými spôsobmi:

  • Ručné naplnenie v administrácii
  • Upload súboru v administrácii
  • Použiť Algolia API

My sme si zvolili samozrejme tretiu možnosť, keďže sa jedná o veľké množstvo dát a proces je potrebné automatizovať. Keďže na strane backendu využívame Python API, použili sme oficiálnu knižnicu algoliasearch priamo od Algolie.

Najskôr je potrebné nadviazať spojenie s API a autentifikovať sa. To sa robí nasledovne:

from algoliasearch.search_client import SearchClient
client = SearchClient.create(app_id, api_key)

app_id a api_key je možné nájsť v administračnom rozhraní. Algolia obsahuje niekoľko rôznych typov API kľúčov. My potrebujeme použiť Write API Key, keďže ideme dáta zapisovať. Po úspešnom nadviazaní spojenia a autentifikácii si vyberieme konkrétny index.

index = client.init_index(index_name)

Následne už pridáme pripravené záznamy do indexu:

index.save_objects(products_algolia)

Ako parameter funkcie save_objects(data) sa zadáva pole objektov reprezentujúce jednotlivé produkty. Pole môže vyzerať napríklad takto:

[
{
"objectID": 1572126,
"name": "Produkt 1",
"description": "Popis pre produkt 1",
"price": 351.25,
"availability": 8
},
{
"objectID": 1572185,
"name": "Produkt 2",
"description": "Nejaký popis pre produkt 2",
"price": 199.9,
"availability": 0
}
]

Hodnota objectID je veľmi dôležitá, je to unikátny identifikátor pre záznam. V prípade, že znovu zavoláme funkciu save_objects() so záznamom s rovnakým objectID, Algolia tieto objekty pokladá za rovnaký záznam a prepíše tak dáta v pôvodnom zázname. Funkcia save_objects teda pracuje ako upsert. Ak daný záznam s daným objectID neexistuje, vytvorí ho. Ak existuje, len zaktualizuje dáta pre pôvodný záznam. My používame ako objectID objednávacie číslo produktu, ktoré je unikátne pre každý produkt.

Po úspešnom pridaní záznamov do databázy je možné okamžite začať testovať a ladiť vyhľadávanie! To je veľká výhoda, lebo výsledky vidíme hneď, bez toho aby sme čo i len začali implementovať vyhľadávanie na stránku. U nás sme teda začali pracovať na implementácii autocomplete, zatiaľ čo zákazník už mohol ladiť vyhľadávanie aby splnilo jeho požiadavky.

Aktualizácia dát

Okrem jednorazového naplnenia indexu je samozrejme potrebná aj pravidelná aktualizácia dát. Jednak nám v eshope pribúdajú nové produkty, ale aj informácie pri starých produktoch (cena, dostupnosť a pod.) je potrebné udržiavať pokiaľ možno čo najaktuálnejšie. My robíme pravidelnú aktualizáciu produktov 1x denne. Z Conrad API získame kazdú noc informáciu o tom, ktoré produkty sa za posledný deň zmenili. Tieto produkty následne prejdeme a zaktualizujeme ich v Algolii. Pravidelnú aktualizáciu produktov na bete neriešime, tá slúži len na testovacie účely a v prípade potreby ju zaktualizujeme ručne.

Okrem pravidelnej aktualizácie vykonávame v prípade potreby aj okamžitú aktualizáciu. Napríklad v prípade, že administrátor zmení kľúčové slová pre produkt. Takáto zmena sa v Algolii prejaví okamžite a administrátor tak má možnosť vyskúšať si vyhľadanie pre zmenené kľúčové slová.

Autocomplete

Na implementáciu autocomplete funkcionality sme sa rozhodli použiť JS knižnicu InstantSearch priamo od Algolie. Výhoda tohto riešenia je hlavne rýchlosť. Keďže vyhľadávanie nie je potrebné spracovávať na strane backendu, výsledky vyhľadávania sa tak okamžite zobrazia užívateľovi.

A keďže pripravená knižnica obsahuje všetko potrebné tak aj implementácia je veľmi rýchla a zobrazenie výsledkov je tak otázka pár hodín roboty aj s prestávkou na kávu. Najskôr musíme na stránku pridať knižicu pre instantseach a pre algoliasearch.

<script src="https://cdn.jsdelivr.net/npm/algoliasearch@3.35.1/dist/algoliasearchLite.min.js" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/instantsearch.js@4.0.0/dist/instantsearch.production.min.js" crossorigin="anonymous"></script>

Aj v javascripte je najskôr potrebné nadviazať spojenie s Algoliou a autentifikovať sa. Tu však treba dať veľký pozor! apiKey je iný ako v prípade pridávania záznamov. Teraz je potrebné použiť Search API Key. To preto, lebo tento kľúč sa nachádza na strane frontendu a ktokoľvek si ho môže pozrieť a použiť. Preto nemôžeme použiť kľúč ktorý umožňuje zmenu dát.

var searchClient = algoliasearch(appId, apiKey);

Následne si inicializujeme instantsearch kde nastavíme názov indexu.

var search = instantsearch({
indexName: 'prod_products',
searchClient: searchClient,
});

Potom pridáme widget pre input so zadanou požiadavkou.

search.addWidget(
instantsearch.widgets.searchBox({
container: '#algolia-search-input-container',
placeholder: 'Search...',
showReset: false,
showLoadingIndicator: false,
autofocus: true
})
);

Ďalej je potrebné pridať ďalší widget a to na zobrazenie výsledkov.

search.addWidget(
instantsearch.widgets.hits({
container: '#algolia-results',
templates: {
item: '<a data-item-number="{{item_number}}" href="/hu/{{txtid}}-{{item_number}}.html">'+
'<div class="img">'+
'{{#photo}}'+
'<img src="{{photo}}?x=41&y=41" align="left" />'+
'{{/photo}}'+
'</div>'+
'<div class="info">'+
'<div class="name" title="{{name}}">'+
'{{#helpers.highlight}}{ "attribute": "name" }{{/helpers.highlight}}'+
'</div>'+
'</div>'+
'</a>'
}
})
);

Následne už len spustíme vyhľadávanie a to je všetko! Po spustení stránky by mal autocomplete fungovať.

search.start();

Podstránka pre vyhľadané produkty

Podstránka pre vyhľadané produkty už obsahuje aj backend implementáciu. To z toho dôvodu, že v Algolii sa nenachádzajú všetky potrebné dáta pre produkt, ale len také, ktoré súvisia s vyhľadávaním. Taktiež chceme použiť čo najčerstvejšie dáta, teda tie, ktoré sú v databáze.

Proces je preto nasledovný: Z Algolie sa získajú výsledky pre zadaný vyhľadávaný výraz. Následne sa výsledky prejdú a podľa objectID sa načítajú produkty z databázy. Tieto produkty sa následne zobrazia na podstránke.

Na vyhľadanie produktov používame rovnakú knižicu ako na ich pridanie do algolie. Taktiež nadviazanie spojenia a výber indexu je rovnaký. A keďže nepotrebujeme dáta upravovať, tak ako API kľúč použijeme Search API Key. Samotné vyhľadanie záznamov vyzerá nasledovne:

client = SearchClient.create(app_id, api_key)
index = client.init_index(index_name)
page = (offset / limit)
search_options = {
'facets': ['*'],
'hitsPerPage': limit,
'page': page,
'sortFacetValuesBy': 'count'
}
search_response = index.search(query, search_options)

search_options môže obsahovať množstvo rôznych ďalších parametrov upravujúcich vyhľadanie. Najlepšie je pozrieť sa priamo do dokumentácie aké sú možnosti a čo ktorý parameter nastavuje.

Zoznam vyhľadaných výsledkov sa nachádza v atribúte hits. A teda napríklad zoznam objednávkových čísel vyhľadaných produktov tak vieme získať nasledovne:

product_numbers = []
for hit in search_response.get('hits', []):
product_numbers.append(hit['objectID'])

Implementácia fazetového vyhľadávania

Možnosť filtrovať vyhľadané produkty podľa konkrétnych parametrov bola pre nás extrémne dôležitá. Vo svete elektronických súčiastok sa parametrické vyhľadávanie používa veľmi často a bez tohto typu filtrovania by vyhľadávanie nemalo zmysel.

Algolia samozrejme umožňuje filtrovať výsledky podľa parametrov a takýto typ filtrovania je označovaný ako fazetové filtrovanie. Okrem možnosti vyfiltrovať len výsledky s daným parametrom nám pre každé vyhľadávanie príde aj zoznam všetkých použiteľných parametrov spolu s číslom, koľko výsledkov nám ostane po pridaní daného parametra.

Pre použitie fazetového filtrovania je najskôr potrebné označiť, ktoré polia sa majú považovať za fazetový filter. U nás sme pre každý záznam vytvorili pole facets a v ňom sa nachádza zoznam parametrov. Kľúč je názov parametra a hodnota je jeho hodnota.

Príklad fazetov pre vybraný produkt:

{
"facets": {
"CATEGORY": "USB-s töltőkészülékek",
"BRAND": "VOLTCRAFT",
"PRICE": 2190,
"RATING": 3,
"ATT.OUTPUTS_LoV": "USB",
"ATT.NUMBER_OUTPUTS": "2x",
"ATT.PLACE_OF-ACTION": "Személygépkocsi",
"ATT.OUTPUT_CURRENT_MAX_PER_CHANNEL": "1200mA",
"ATT.NUM.OUTPUT_CURRENT_MAX": "2400mA"
}
}

Okrem samotných parametrov produktu vkladáme medzi fazety aj cenu produktu, kategóriu, hodnotenie a značku. V Algolii je potrebné nastaviť ktoré polia majú slúžiť na fazetové filtrovanie. Dá sa to nastaviť napríklad aj cez administračné rozhranie, ale takýto spôsob je vhodný len v prípade že nemáme veľa filtrov. U nás sa nachádza viac ako 1000 filtrov a nastavenie sa teda robí cez API použítím funkcie set_settings()

index.set_settings({
'attributesForFaceting': facet_names
})

Pri vyhľadaní vieme parameter (fazet) pridať cez atribút facetFilters, čo je vlastne pole fazetov. Každý fazet je pole textových reťazcov v tvare: názov_facetu:hodnota. Napríklad pre vyhľadanie produktov ktoré majú hodnotenie 4* použijeme nasledovný kód:

search_options = {
'facets': ['*'],
'hitsPerPage': limit,
'page': page,
'sortFacetValuesBy': 'count',
'facetFilters': [['facets.RATING:4']]
}
search_response = index.search(query, search_options)

Fazety vieme samozrejme aj kombinovať, napríklad vyhľadať produkty ktoré majú hodnotenie 4 alebo 5 a zároveň majú príkon 200W a podobne. Možností je tu naozaj veľa, najlepšie je zase pozrieť v dokumentácii a inšpirovať sa.

Nevýhody Algolia Search

Algolia má, tak ako každé vyhľadávanie aj svoje nevýhody. Medzi najvážnejšie nedostatky Algolia search považujeme nemožnosť zoradiť vyhľadané výsledky podľa niektorého z atribútov. Napríklad zoradiť produkty od najlacnejšieho po najdrahší.

Na to, aby bolo možné výsledky zoradiť je potrebné spraviť repliku indexu. Replikovaný index je následne previazaný s originálnym indexom a každá zmena záznamu v origináli spôsobí aj zmenu v replike. Hlavný problém spočíva v tom, že počet záznamov každej repliky sa počíta do celkového limitu záznamov. V našom prípade máme limit 1 000 000 záznamov. Ale len v samotnej v produkčnej databáze sa nachádza cez 450 000 produktov, podobné množstvo sa nachádza aj na testovacej databáze. Po sčítaní sa to teda len tak tak zmestí do limitu a neostane už ďalšie miesto pre repliky. Tám pádom sme na stránke nemohli použiť zoraďovanie výsledkov. Dá sa to sčasti riešiť cez vhodné filtre. Napríklad namiesto zoraďovania produktov podľa ceny sme implementovali posuvník s najvyššou a najnižšou cenou spomedzi vyhľadaných produktov. Užívateľ tak má možnosť zobraziť si len tie cenové hladiny, ktoré mu vyhovujú.

Záver

S vyhľadávaním cez Algolia Search sme aj napriek spomínaným nedostatkom veľmi spokojný. Vyhľadávanie je rýchle, spoľahlivé a zákazník má možnosť si cez administračné rozhranie konfigurovať skoro čokoľvek, bez nutnosti spolupráce programátora. Implementácia celého riešenia, od naplnenia dát, implementácie autocomplete a podstránky s vyhľadanými produktami až po odladenie a spustenie na produkcii zabrala len nejakých 40 hodín roboty.

Neprehliadnite výsledky našej programátorskej práce.