Skip to content

Príklad: vlastná výsledková stránka ​

Výsledková stránka s facetmi, radením a stránkovaním nad Search API. Volanie prebieha zo servera, takže verejný kľúč nemusí opustiť backend a odpoveď sa dá cachovať.

Nasledujúci kód je Symfony aplikácia; samotný klient frameworkovú závislosť nemá, takže v inom frameworku sa mení iba obal controllera.

Klient ako služba ​

yaml
# config/services.yaml
services:
    Metty\Client\MettyClient:
        factory: ['Metty\Client\MettyClient', 'create']
        arguments:
            $publicKey: '%env(METTY_PUBLIC_KEY)%'

Na výsledkovú stránku stačí verejný kľúč — iba číta. Secret kľúč patrí k importu katalógu, nie sem.

Backend ​

php
<?php

namespace App\Controller;

use Metty\Client\Exception\ConfigurationException;
use Metty\Client\MettyClient;
use Metty\Client\Search\SearchQuery;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;

final class SearchController extends AbstractController
{
    private const PER_PAGE = 24;

    public function __construct(private readonly MettyClient $metty) {}

    #[Route('/search', name: 'search', methods: ['GET'])]
    public function __invoke(Request $request): Response
    {
        $text = (string) $request->query->get('q', '');
        $active = $request->query->all('filter');

        try {
            $query = SearchQuery::for($text)
                ->perPage(self::PER_PAGE)
                ->page(max(1, $request->query->getInt('page', 1)))
                ->withSections('facets', 'categories', 'suggestions');

            $sort = (string) $request->query->get('sort', '');
            if (in_array($sort, SearchQuery::SORTS, true)) {
                $query = $query->sortBy($sort);
            }

            $category = (string) $request->query->get('category', '');
            if ($category !== '') {
                $query = $query->category($category);
            }

            foreach ($active as $field => $values) {
                foreach ((array) $values as $value) {
                    $query = $query->facet((string) $field, (string) $value);
                }
            }

            $response = $this->metty->search()->search($query);
        } catch (ConfigurationException) {
            return $this->redirectToRoute('search', ['q' => $text]);
        }

        return $this->render('search.html.twig', [
            'results' => $response,
            'active' => $active,
        ]);
    }
}

Skladanie dotazu je v try tiež, lebo neplatný dotaz klient odmietne ešte pred odoslaním, napríklad stranu za hranicou 200 výsledkov — a číslo strany prichádza priamo z URL. Návrat na prvú stranu je používateľsky prijateľnejší než 500.

Radenie navyše kontrolujeme oproti SearchQuery::SORTS, takže preklep v sort v URL stojí iba radenie, nie celý dotaz.

Zvýraznenie zhody ​

Zhodu označuje server hranatými zátvorkami v poli highlight. Na klientovi ich stačí nahradiť značkou a nič nedopočítavať:

php
<?php

namespace App\Twig;

use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;

final class HighlightExtension extends AbstractExtension
{
    public function getFunctions(): array
    {
        return [
            new TwigFunction('highlight', $this->highlight(...), ['is_safe' => ['html']]),
        ];
    }

    public function highlight(?string $marked, string $fallback): string
    {
        if ($marked === null) {
            return htmlspecialchars($fallback);
        }

        return str_replace(['[', ']'], ['<mark>', '</mark>'], htmlspecialchars($marked));
    }
}

Pri predvolenom autoconfigure: true si Symfony rozšírenie zaregistruje samo, takže funkcia je dostupná v každej šablóne:

twig
<h3>{{ highlight(product.highlight.name|default(null), product.name) }}</h3>

Escapovanie musí prebehnúť pred nahradením zátvoriek, inak by sa do stránky dostalo HTML z názvu produktu. Funkcia vracia už naescapovaný výsledok, preto je označená ako is_safe a v šablóne nepotrebuje |raw.

Facety a filtre ​

facets je zoznam polí a každé nesie strojový názov aj názov na zobrazenie:

twig
{% for facet in results.facets %}
  <fieldset>
    <legend>{{ facet.label }}</legend>
    {% for value in facet.values %}
      <label>
        <input type="checkbox" name="filter[{{ facet.field }}][]" value="{{ value.value }}"
               {{ value.value in active[facet.field]|default([]) ? 'checked' }}>
        {{ value.value }} <span>({{ value.count }})</span>
      </label>
    {% endfor %}
  </fieldset>
{% endfor %}

Viac hodnôt jedného poľa znamená ALEBO, hodnoty rôznych polí sa kombinujú cez A ZÁROVEŇ. Počty v odpovedi sú prepočítané podľa aktívnych filtrov, takže sa dajú zobraziť priamo.

Stránkovanie ​

php
$lastPage = min(
    $response->pages,
    intdiv(SearchQuery::MAX_WINDOW, $response->perPage),
);

Ranguje sa prvých 200 výsledkov, preto je posledná dostupná strana daná touto hranicou, nie celkovým počtom nájdených produktov. Ak potrebujete zákazníkovi ponúknuť viac, zúžte výber kategóriou alebo facetom — hlbšie stránkovanie by aj tak vrátilo neusporiadané poradie.

Volanie priamo z prehliadača ​

Verejný kľúč je určený na čítanie, takže Search API sa dá volať aj z frontendu:

js
const params = new URLSearchParams({
    key: PUBLIC_API_KEY,
    q: query,
    per_page: '24',
    include: 'facets,categories'
})

for (const [field, values] of Object.entries(filters)) {
    for (const value of values) {
        params.append(`filter[${field}][]`, value)
    }
}

const response = await fetch(`https://search.api.metty.eu/search?${params}`)

if (!response.ok) {
    const { error } = await response.json()
    throw new Error(error)
}

const { products, total, facets } = await response.json()

Facety patria pod filter[…] a opakovaná hodnota potrebuje koncové zátvorky (filter[farba][]=modrá&filter[farba][]=čierna). Bez nich si server ponechá iba poslednú hodnotu.

Ak vyhľadávanie voláte pri každom stlačenom znaku, použite radšej našepkávanie — má vyšší rate limit a menšiu odpoveď. Volania z prehliadača sa počítajú do rate limitu IP adresy návštevníka, spolu s widgetom, ak beží na tej istej stránke.

Dokumentácia Metty