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
# 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
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
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:
<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:
{% 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
$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:
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.