Rýchle vyhľadávanie v e-shope: Ako udržať nízku odozvu aj počas Black Friday

Od nolimeo · 21. apríla 2026
banner image

Predstavte si najrušnejší deň v roku. Je Black Friday a vaša B2C e-commerce platforma zažíva masívny nápor návštevníkov. Reklamné kampane bežia na plné obrátky, tisíce ľudí naraz prichádzajú na web s úmyslom nakúpiť. Jeden zo zákazníkov klikne do vyhľadávacieho riadku a napíše: „zimná bunda“.

Stránka na chvíľu zamrzne. Koliesko načítavania sa točí. Zákazník, zvyknutý na rýchle výsledky z Google či YouTube, stráca trpezlivosť, zatvára kartu a odchádza ku konkurencii.

Pri e-commerce platí jednoduché pravidlo: čím dlhšie zákazník čaká na výsledky, tým väčšia je šanca, že nákup nedokončí. Ľudia, ktorí používajú vyhľadávacie pole, majú často vysoký nákupný zámer (high purchase intent). Ak ich v tomto bode sklame rýchlosť webu, časť peňazí investovaných do marketingu sa stratí ešte pred checkoutom.

Prečo tradičné databázy počas záťažových špičiek zlyhávajú, ako vyzerá architektúra rýchleho e-commerce vyhľadávania a kedy dáva zmysel kombinácia Meilisearch a Redis cache vrstvy? To si rozoberieme v tomto článku z pohľadu reálneho inžinierstva technologického štúdia nolimeo.


1. Prečo SQL databáza pri vyhľadávaní produktov kolabuje

Väčšina štandardných e-shopov, najmä tie postavené na WooCommerce či starších monolitických systémoch, sa spolieha na svoju primárnu relačnú databázu (PostgreSQL alebo MySQL) aj pri textovom vyhľadávaní produktov. Pri zadaní hľadaného výrazu backend pošle do databázy dopyt v štýle:

-- Tradičný, pomalý dopyt pri raste databázy
SELECT * FROM products 
WHERE (name LIKE '%zimna%' OR description LIKE '%zimna%') 
  AND (name LIKE '%bunda%' OR description LIKE '%bunda%')
  AND is_active = true;

Tento prístup môže pri nízkej návštevnosti fungovať, no počas Black Friday sa mení na výkonnostné riziko. Dôvody sú tri:

A. Chýbajúca optimalizácia pre Full-Text

Operátor LIKE s divokou kartou na začiatku výrazu (%bunda%) znemožňuje PostgreSQL efektívne použiť štandardné B-Tree indexy. Databáza musí vykonať prečítanie celej tabuľky riadok po riadku (Full Table Scan). Pri väčšom katalógu to znamená zbytočne veľa práce pre primárnu databázu.

B. Saturácia databázového Connection Poolu

Ak veľa používateľov naraz odošle dopyt na vyhľadávanie, databáza môže vyčerpať voľné pripojenia (connection pool). Keď vyhľadávacie dopyty trvajú príliš dlho, nové pripojenia čakajú v rade. Výsledkom nie je len pomalé vyhľadávanie, ale aj riziko spomalenia checkoutu a zápisu objednávok.

C. Jazyková bariéra (Flexia a diakritika)

Relačné databázy samy od seba nerozumejú slovenskej gramatike. Ak zákazník napíše „zimné bundy“, jednoduchý LIKE dopyt nemusí nájsť produkt s názvom „zimná bunda“, pretože reťazec sa presne nezhoduje. Riešiť toto pomocou regulárnych výrazov alebo tsvector dopytov v PostgreSQL síce ide, ale vyžaduje dobrý návrh indexov a dodatočnú údržbu.

Riešením je oddelenie (decoupling) vyhľadávacej vrstvy od hlavnej transakčnej databázy.


2. Architektúra moderného vyhľadávania: Prečo volíme Meilisearch

Namiesto zaťažovania hlavnej databázy skopírujeme údaje o produktoch (názov, popis, cena, sklad, atribúty) do špecializovaného, pamäťovo optimalizovaného vyhľadávacieho enginu. Backend následne smeruje všetky dopyty zákazníkov sem.

 Zákazník vyhľadáva ──► [ Next.js Storefront ] ──► [ Meilisearch (nízka odozva) ]
                                                            ▲
                                                            │ (Synchrónny update)
 [ ERP / Administrácia ] ──► [ Primárna DB (Postgres) ] ────┘

Na trhu existujú tri hlavné alternatívy pre full-text vyhľadávanie:

  1. Algolia: Špičkový cloudový SaaS produkt s veľmi rýchlou odozvou. Jeho nevýhodou sú však ťažšie predvídateľné licenčné poplatky pri raste počtu produktov a dopytov, ktoré môžu úspešný e-shop výrazne predražiť.
  2. Elasticsearch / OpenSearch: Priemyselný štandard s rozsiahlymi možnosťami nastavenia. Pre bežný a stredne veľký e-shop však môže byť zbytočne ťažký na prevádzku. Vyžaduje viac infraštruktúrnej disciplíny, pamäte a skúseností s ladením.
  3. Meilisearch: Častá voľba v nolimeu pre e-commerce katalógy. Je napísaný v jazyku Rust, je rýchly, ľahší než Elasticsearch a zameriava sa na vyhľadávanie počas písania (search-as-you-write), toleranciu preklepov a prácu s diakritikou.

3. Technická implementácia: Real-time synchronizácia databázy s Meilisearch

Pre úspešné fungovanie vyhľadávacieho enginu musíme zabezpečiť, že zmeny produktu v e-commerce databáze, napríklad zmena ceny, názvu alebo skladovej dostupnosti, sa spoľahlivo premietnu do Meilisearch indexu.

Ukážeme si praktický TypeScript skript (Medusa.js / Node.js subscriber vzor), ktorý asynchrónne zachytáva udalosti úpravy produktov a vykonáva čiastkový (incremental) update do Meilisearch.

Krok 1: Sync Subscriber v TypeScript

Tento kód beží ako pozadná služba a počúva na event zmeny produktu.

// src/subscribers/product-search-sync.ts
import { MeiliSearch } from "meilisearch";
import { z } from "zod";

// Zod schéma pre overenie integrity produktu pred odoslaním do indexu
const SearchProductSchema = z.object({
  id: z.string(),
  title: z.string().min(2),
  description: z.string().nullable().optional(),
  handle: z.string(),
  thumbnail: z.string().url().nullable().optional(),
  price_eur: z.number().positive(),
  inventory_quantity: z.number().int(),
  tags: z.array(z.string()).default([]),
});

type SearchProduct = z.infer<typeof SearchProductSchema>;

const meiliClient = new MeiliSearch({
  host: process.env.MEILISEARCH_HOST || "http://127.0.0.1:7700",
  apiKey: process.env.MEILISEARCH_ADMIN_KEY || "masterKey",
});

/**
 * Pozadný worker reagujúci na aktualizáciu produktu v primárnej databáze
 */
export async function handleProductUpdateEvent(productId: string, dbService: any): Promise<void> {
  console.log(`[SEARCH SYNC] Inicializujem indexáciu pre produkt ID: ${productId}`);

  try {
    // 1. Načítame produkt z hlavnej PostgreSQL databázy
    const rawProduct = await dbService.products.retrieve(productId, {
      relations: ["variants", "variants.prices", "tags"],
    });

    if (!rawProduct || !rawProduct.is_active) {
      // Ak produkt neexistuje alebo bol deaktivovaný, vymažeme ho z vyhľadávacieho indexu
      await meiliClient.index("products").deleteDocument(productId);
      console.log(`[SEARCH SYNC] Produkt ${productId} bol úspešne vymazaný z indexu.`);
      return;
    }

    // 2. Transformujeme relačné SQL dáta do plochého JSON dokumentu pre Meilisearch
    const lowestPrice = Math.min(...rawProduct.variants.map((v: any) => v.prices.find((p: any) => p.currency_code === "eur")?.amount || 999999)) / 100;
    const totalInventory = rawProduct.variants.reduce((sum: number, v: any) => sum + (v.inventory_quantity || 0), 0);

    const flatProduct: SearchProduct = {
      id: rawProduct.id,
      title: rawProduct.title,
      description: rawProduct.description || "",
      handle: rawProduct.handle,
      thumbnail: rawProduct.thumbnail || null,
      price_eur: lowestPrice,
      inventory_quantity: totalInventory,
      tags: rawProduct.tags.map((t: any) => t.value),
    };

    // 3. Validujeme dáta pomocou Zod schémy, aby sme zabránili zapísaniu nekonzistentných dát
    const validatedProduct = SearchProductSchema.parse(flatProduct);

    // 4. Vykonáme asynchrónny zápis (upsert) do Meilisearch indexu
    const task = await meiliClient.index("products").addDocuments([validatedProduct]);
    
    console.log(`[SEARCH SYNC SUCCESS] Produkt ${productId} indexovaný. MeiliTask ID: ${task.taskUid}`);

  } catch (error) {
    console.error(`[SEARCH SYNC ERROR] Zlyhala synchronizácia pre produkt ${productId}:`, error);
    // V produkčnom prostredí tu nasleduje odoslanie chyby do Sentry a uloženie úlohy do DB pre neskorší pokus (Retry Queue)
  }
}

Krok 2: Next.js API Route pre rýchle autocomplete vyhľadávanie

Teraz vytvoríme Next.js API Route, ktorá prijíma dopyty z prehliadača zákazníka, komunikuje s Meilisearch a vracia výsledky bez zaťaženia primárnej databázy.

// src/app/api/search/route.ts
import { NextRequest, NextResponse } from "next/server";
import { MeiliSearch } from "meilisearch";

const meiliClient = new MeiliSearch({
  host: process.env.MEILISEARCH_HOST || "http://127.0.0.1:7700",
  apiKey: process.env.MEILISEARCH_SEARCH_KEY || "searchKey", // Bezpečný verejný kľúč len pre čítanie
});

export async function GET(req: NextRequest): Promise<NextResponse> {
  const { searchParams } = new URL(req.url);
  const query = searchParams.get("q") || "";

  if (query.trim().length < 2) {
    return NextResponse.json({ hits: [] });
  }

  try {
    // Spustíme hľadanie v indexe produktov
    const searchResult = await meiliClient.index("products").search(query, {
      limit: 8,
      attributesToRetrieve: ["id", "title", "handle", "thumbnail", "price_eur", "inventory_quantity"],
      attributesToHighlight: ["title"], // Zvýraznenie hľadaného slova v názve produktu
      filter: ["inventory_quantity > 0"], // Zobrazujeme len produkty, ktoré sú reálne na sklade
    });

    return NextResponse.json({
      hits: searchResult.hits,
      processingTimeMs: searchResult.processingTimeMs, // Uvidíte odozvu priamo v milisekundách
    });

  } catch (error) {
    console.error("Chyba pri vyhľadávaní v Meilisearch:", error);
    return NextResponse.json({ error: "Vyhľadávanie je momentálne nedostupné." }, { status: 500 });
  }
}

4. Redis cache pre najčastejšie dopyty

Aj keď Meilisearch vie pri dobre navrhnutom indexe odpovedať veľmi rýchlo, počas výrazného preťaženia, napríklad na Black Friday, dáva zmysel pridať ešte cache vrstvu.

Zákazníci často vyhľadávajú rovnaké populárne frázy (napr. „bunda“, „iphone“, „darček“). Namiesto spúšťania rovnakého vyhľadávania znova a znova môžeme výsledky pre tieto „horúce“ kľúčové slová uložiť do distribuovanej pamäte Redis na 5 minút.

 [ Zákazník ] ──► [ Next.js API ] ──► Overí v REDIS (In-Memory Key-Value)
                                              │
                     ┌────────────────────────┴────────────────────────┐
                     ▼ (Zásah - Hit)                                    ▼ (Minutie - Miss)
             Vráti výsledok z pamäte                           Opýta sa Meilisearch & zapíše do Redis

S Redis cache vieme odľahčiť vyhľadávací server pri opakovaných populárnych dopytoch a znížiť tlak na infraštruktúru počas marketingových kampaní.


5. Riešenie hraničných stavov: preklepy, synonymá a slovenská gramatika

Slovenský jazyk prináša do e-commerce vyhľadávania špecifické výzvy. Ako ich ošetriť inžiniersky správne?

5.1. Ohýbanie slov (Lemmatization a diakritická tolerancia)

Slováci píšu do vyhľadávania rýchlo a bez dĺžňov či mäkčeňov. Hľadajú „ciapka“, „čiapky“ alebo „čiapku“.

  • Riešenie v Meilisearch: Meilisearch má natívnu podporu pre multijazyčnú tokenizáciu a normalizáciu diakritiky. Znak č automaticky mapuje na c a í na i. Pri indexácii navyše vieme nastaviť zoznam synoným tak, aby vyhľadanie jednotného a množného čísla viedlo k rovnakým výsledkom.

5.2. Synonýmický slovník (Zákazník nevie presný názov)

Niekto hľadá „notebook“, niekto iný „prenosný počítač“ alebo „laptop“. Ak nemáte nastavené synonymá, zákazník uvidí smutnú stránku „Nenašli sa žiadne produkty“, hoci ich máte na sklade desiatky.

  • Riešenie: V Meilisearch indexe nastavíme konfiguračný objekt synoným:
{
  "synonyms": {
    "notebook": ["laptop", "prenosny pocitac"],
    "laptop": ["notebook", "prenosny pocitac"],
    "bunda": ["vetrovka", "kabát"]
  }
}

6. Prečo ignorovať "klikacie" pluginy pre vyhľadávanie

Pri hľadaní riešenia rýchleho vyhľadávania sa môžete stretnúť s ponukami rôznych pluginov tretích strán, ktoré sa dajú nainštalovať do WooCommerce či iných systémov na jeden klik.

Tieto pluginy sú však pre rastúci e-shop technologickou pascou:

  1. Bežia na rovnakom hardvéri: Väčšina z nich funguje tak, že spúšťa o niečo lepšie SQL dopyty, no stále zaťažuje ten istý databázový server, ktorý poháňa váš e-shop. Počas záťaže tak problém často iba posunú, nie vyriešia.
  2. Sú to čierne skrinky: Nemáte žiadnu kontrolu nad tým, ako sa indexy aktualizujú, nemôžete optimalizovať zložité transakčné scenáre a ak sa niečo pokazí, ste odkázaní na pomalú zákaznícku podporu tvorcov pluginu.

Záver: Vyhľadávanie musí vydržať aj špičku

Rýchlosť e-shopu je jeden z neviditeľných faktorov, ktoré rozhodujú o úspechu marketingových kampaní. Prechod na dedikovaný vyhľadávací systém postavený na Meilisearch, dobre navrhnutej indexácii a prípadne Redis cache vrstve odľahčí primárnu databázu od záťažových dopytov a pomôže zákazníkom rýchlejšie nájsť produkt, ktorý prišli kúpiť.

Sme technologické štúdio nolimeo. Navrhujeme headless e-commerce architektúru, produktové vyhľadávanie a backend integrácie pre firmy, ktoré potrebujú zvládať väčšie katalógy, vyššiu návštevnosť a sezónne špičky.

Trápi vás pomalé vyhľadávanie na e-shope alebo databáza počas kampaní nestíha? Napíšte nám a prejdeme si katalóg, záťažové miesta aj vhodnú architektúru vyhľadávania.

Máte záujem posunúť váš projekt vpred?