Startseite
Developer API v1

querrio Search API v1 — Hybrid Retrieval mit LLM-Reranking als REST-Endpoint

Pipeline pro Request: Intent-Parsing → Hybrid Retrieval (Postgres-Volltext + pgvector) → LLM-Reranking bei knappem Score-Abstand. Die Antwort ist JSON mit top_hits, results, groups, suggestions und click_url je Treffer. Auth per Browser-Key (pk_…, Origin-gebunden) oder Server-Key (sk_…). Die Sprache wird gegen die Projektsprachen aufgelöst. Indexierung und Ranking-Tuning laufen serverseitig.

1 · Widget (Drop-in)

Ein Script-Tag, rendert in Shadow DOM ohne CSS-Konflikte. Konfiguration im Dashboard, Overrides per Custom CSS.

<script src="https://cdn.querrio.com/widget.js"
  data-key="pk_dein_browser_key" defer></script>

2 · JS-SDK (headless)

~2 KB, keine Dependencies, Typdefinitionen unter /sdk.d.ts. Enthält Debouncing, Session-ID, Abort von Suggest-Requests und Tracking per sendBeacon.

<script src="https://cdn.querrio.com/sdk.js"></script>
<script>
  const client = querrio.createClient({ key: 'pk_dein_browser_key' });

  const res = await client.search('beispiel suchanfrage');
  // res.top_hits    → 1–2 Treffer für die Teaser-Darstellung
  // res.results     → restliche Treffer als Liste
  // res.groups      → Kategorien mit Anzahl und Treffer-IDs
  // res.suggestions → "Häufig gesucht" als Tags
  // res.intent      → erkannte Absicht, Zeitraum, Zielgruppe

  client.trackClick(res.results[0], 1);
</script>

TypeScript-Typen

Die vollständigen Typen (inkl. Kategorien, Tier-Gewichtung, Intent und Debug-Objekt) liegen unter /sdk.d.ts. Datei ablegen oder per curl ins Projekt ziehen — danach greifen Autocompletion und Typprüfung für SDK und REST-Antworten.

curl -o types/querrio.d.ts https://cdn.querrio.com/sdk.d.ts

// app.ts
/// <reference path="./types/querrio.d.ts" />

const client = window.Querrio.createClient({ key: 'pk_dein_browser_key' });

const res: querrio.SearchResponse = await client.search('beispiel suchanfrage');
res.top_hits[0].tier;        // "top" | "list"
res.groups[0].category;      // "event" | "article" | "product" | …
res.intent.date_from;        // "2026-04-19" oder null
client.trackClick(res.results[0], 1);

Anzeige vor der ersten Eingabe: Themen-Tags & Häufig gesucht

Alles, was vor der ersten Eingabe angezeigt wird, liefert GET /api/public/v1/schema. Die Listen pflegt der Autopilot automatisch (oder dein Team manuell im Backend) – pro Website-Sprache.

FeldInhaltWo anzeigen
widget.topic_tags_by_langKurze Themen (1–3 Wörter, max. 8), z. B. „Theater“, „Konzerte“Als Tags direkt unter dem Suchfeld, immer sichtbar
widget.quick_chips_by_langSuchphrasen (max. 6), z. B. „Abonnement verlängern“ – aus echten Suchen, in der Startphase KI-VorschlägeIm leeren Suchfeld unter „Häufig gesucht“
entry_pointsWichtigste Zielseiten (url, title, page_kind)Optional als Schnelleinstiege im leeren Suchfeld
widget.config.placeholdersPlatzhaltertext je Spracheplaceholder des Suchfelds
  • Sprache: Immer die Liste der aktuellen Seitensprache nehmen. Gibt es keine, nichts anzeigen – nie eine andere Sprache. topic_tags/quick_chips ohne _by_lang enthalten nur die Hauptsprache.
  • Klick: Begriff als Suche an /v1/search senden und als suggest_click melden – sonst lernt der Autopilot nicht mit.
  • Caching: Die Listen ändern sich höchstens täglich; 1 Stunde Cache im Browser ist unproblematisch.
// GET /api/public/v1/schema  (Header: x-querrio-key: pk_…)
{
  "languages": ["de", "en"],
  "entry_points": [{ "url": "https://…/abo", "title": "Abonnements", "page_kind": "page" }],
  "widget": {
    "primary_language": "de",
    "config": { "placeholders": { "de": "Stellen Sie eine Frage", "en": "Ask a question" } },
    "topic_tags_by_lang":  { "de": ["Kulturfahrten", "Theater", "Abonnements", "Konzerte"], "en": ["Theatre", "Concerts"] },
    "quick_chips_by_lang": { "de": ["Abonnement verlängern", "Veranstaltungen für Kinder"], "en": ["Renew subscription"] },
    "topic_tags":  ["Kulturfahrten", "Theater"],          // Hauptsprache (Kompatibilität)
    "quick_chips": ["Abonnement verlängern"]             // Hauptsprache (Kompatibilität)
  }
}
const KEY = "pk_…", API = "https://api.querrio.com";
const lang = (document.documentElement.lang || "de").slice(0, 2);
const s = await fetch(API + "/api/public/v1/schema", { headers: { "x-querrio-key": KEY } }).then(r => r.json());
const pick = (byLang) => byLang?.[lang] ?? []; // nie eine fremde Sprache anzeigen

input.placeholder = s.widget.config.placeholders?.[lang] ?? input.placeholder;
tagRow.innerHTML = pick(s.widget.topic_tags_by_lang)
  .map(t => `<button class="tag">${t}</button>`).join("");

tagRow.addEventListener("click", (e) => {
  const t = e.target.closest(".tag"); if (!t) return;
  input.value = t.textContent;
  runSearch(input.value);                 // POST /api/public/v1/search
  fetch(API + "/api/public/v1/track", {   // Klick zählt für Autopilot
    method: "POST", headers: { "content-type": "text/plain" },
    body: JSON.stringify({ key: KEY, kind: "suggest_click" }), // Begriff selbst kommt über die Suche
  });
});

// Im leeren Suchfeld (Fokus, noch nichts getippt): pick(s.widget.quick_chips_by_lang)
function SearchStart({ schema, lang, onSearch }) {
  const tags = schema.widget.topic_tags_by_lang?.[lang] ?? [];
  return (
    <div className="tags">
      {tags.map(t => (
        <button key={t} onClick={() => { onSearch(t); client.trackSuggestClick(t); }}>{t}</button>
      ))}
    </div>
  );
}

Referenz-Implementierungen: Vanilla JS, React, Server

Vollständige Headless-Integration: Suggest-Input, Top-Hit-Rendering, Facetten-Filter über groups, Quick-Chips, Zero-Result-State und Klick-Tracking. Key ersetzen, lauffähig.

<!doctype html>
<html lang="de">
<head>
  <meta charset="utf-8" />
  <title>Eigene Suche mit querrio</title>
  <script src="https://cdn.querrio.com/sdk.js"></script>
  <style>
    body { font-family: system-ui, sans-serif; max-width: 720px; margin: 40px auto; padding: 0 16px; }
    input { width: 100%; padding: 12px 16px; border: 1px solid #ccc; border-radius: 999px; font-size: 16px; }
    .suggest { list-style: none; padding: 0; margin: 4px 0; border: 1px solid #eee; border-radius: 12px; }
    .suggest li { padding: 8px 14px; cursor: pointer; }
    .suggest li:hover { background: #f4f4f5; }
    .chips button { margin: 4px 4px 0 0; padding: 4px 12px; border: 1px solid #ddd; border-radius: 999px; background: #fff; cursor: pointer; }
    .chips button.active { background: #111; color: #fff; }
    .hit { display: flex; gap: 12px; padding: 12px 0; border-bottom: 1px solid #eee; text-decoration: none; color: inherit; }
    .hit img { width: 96px; height: 64px; object-fit: cover; border-radius: 8px; }
    .top { background: #f7f7f8; border-radius: 12px; padding: 12px; }
    small { color: #666; }
  </style>
</head>
<body>
  <input id="q" type="search" placeholder="Wonach suchst du?" autocomplete="off" />
  <ul id="suggest" class="suggest" hidden></ul>
  <div id="filters" class="chips"></div>
  <div id="out"></div>
  <p><small>Suche von querrio</small></p>

  <script>
    const client = querrio.createClient({ key: "pk_dein_browser_key" });
    const $ = (id) => document.getElementById(id);
    let last = null;       // letzte Antwort
    let activeCat = null;  // gewählter Kategorie-Filter
    let timer;

    // Vorschläge beim Tippen
    $("q").addEventListener("input", () => {
      clearTimeout(timer);
      const q = $("q").value.trim();
      if (q.length < 2) { $("suggest").hidden = true; return; }
      timer = setTimeout(async () => {
        const s = await client.suggest(q);
        if (!s.enabled || !s.items.length) { $("suggest").hidden = true; return; }
        $("suggest").innerHTML = s.items
          .map((i) => `<li data-text="${i.text}">${i.text}</li>`).join("");
        $("suggest").hidden = false;
      }, 150);
    });

    $("suggest").addEventListener("click", (e) => {
      const text = e.target.dataset.text;
      if (!text) return;
      client.trackSuggestClick(text);
      $("q").value = text;
      $("suggest").hidden = true;
      run(text);
    });

    $("q").addEventListener("keydown", (e) => {
      if (e.key === "Enter") { $("suggest").hidden = true; run($("q").value); }
    });

    async function run(query) {
      if (!query.trim()) return;
      $("out").innerHTML = "<small>Suche läuft …</small>";
      activeCat = null;
      last = await client.search(query);
      render();
    }

    function hitHtml(h, pos, top) {
      return `<a class="hit ${top ? "top" : ""}" href="${h.url}" data-pos="${pos}" data-id="${h.id}">
        ${h.image_url ? `<img src="${h.image_url}" alt="" />` : ""}
        <div><strong>${h.title}</strong><br /><small>${h.snippet ?? ""}</small></div>
      </a>`;
    }

    function render() {
      const r = last;
      const all = [...r.top_hits, ...r.results];

      // Keine Treffer: vorgeschlagene Begriffe anbieten
      if (!all.length) {
        $("filters").innerHTML = "";
        $("out").innerHTML = `<p>Noch nichts Passendes gefunden. Versuch es mit:</p>
          <div class="chips">${r.suggestions.map((s) => `<button data-q="${s}">${s}</button>`).join("")}</div>`;
        return;
      }

      // Kategorie-Filter aus den Gruppen
      $("filters").innerHTML = r.groups.map((g) =>
        `<button data-cat="${g.category}" class="${g.category === activeCat ? "active" : ""}">${g.label} (${g.count})</button>`
      ).join("");

      const visible = activeCat
        ? all.filter((h) => r.groups.find((g) => g.category === activeCat)?.hit_ids.includes(h.id))
        : null;

      $("out").innerHTML =
        (r.date_label ? `<small>Zeitraum: ${r.date_label}</small>` : "") +
        (visible
          ? visible.map((h, i) => hitHtml(h, i + 1, false)).join("")
          : r.top_hits.map((h, i) => hitHtml(h, i + 1, true)).join("") +
            r.results.map((h, i) => hitHtml(h, r.top_hits.length + i + 1, false)).join("")) +
        (r.suggestions.length
          ? `<p><small>Häufig gesucht</small></p><div class="chips">${r.suggestions
              .map((s) => `<button data-q="${s}">${s}</button>`).join("")}</div>`
          : "");
    }

    // Filter- und Stichwort-Klicks
    document.addEventListener("click", (e) => {
      const t = e.target.closest("button");
      if (!t) return;
      if (t.dataset.cat) { activeCat = activeCat === t.dataset.cat ? null : t.dataset.cat; render(); }
      if (t.dataset.q) { $("q").value = t.dataset.q; run(t.dataset.q); }
    });

    // Klick-Auswertung: Analytics + Autopilot lernen mit
    $("out").addEventListener("click", (e) => {
      const a = e.target.closest("a.hit");
      if (!a || !last) return;
      const hit = [...last.top_hits, ...last.results].find((h) => h.id === a.dataset.id);
      if (hit) client.trackClick(hit, Number(a.dataset.pos));
    });
  </script>
</body>
</html>

Checkliste vor dem Go-live

  • Im Browser nur den Browser-Schlüssel (pk_…) verwenden und deine Domain im Backend unter „API & Widget“ freigeben.
  • Server-Schlüssel ausschließlich auf deinem Server nutzen – nie im Browser-Code.
  • Klick-Auswertung einbauen – ohne sie lernen Analytics und Autopilot nicht mit.
  • Den Hinweis „Suche von querrio“ gemäß deinem Abo sichtbar lassen.

3 · REST-API

Server-Key im Header x-querrio-key. Browser-Keys sind auf die erlaubten Origins beschränkt.

curl -X POST https://api.querrio.com/api/public/v1/search \

  -H 'content-type: application/json' \
  -H 'x-querrio-key: sk_dein_server_key' \
  -d '{ "query": "beispiel suchanfrage", "limit": 10 }'

Tracking bei API-Einbindung (Voraussetzung für den Autopilot)

Suchen und angezeigte Treffer zählt querrio automatisch beim Aufruf von /v1/search. Klicks und Feedback sieht querrio bei einer eigenen Oberfläche nicht von selbst – sie müssen gemeldet werden. Sonst bleiben Klickraten leer. Der Autopilot stuft dann zwar nichts ab, lernt aber auch nicht dazu.

Besucher sucht   → POST /v1/search  → Suche + angezeigte Treffer werden gezählt
                                      Antwort enthält query_id und je Treffer id + click_url
Besucher klickt  → Option A: Link über hit.click_url   (zählt automatisch)
                 → Option B: POST /v1/track { kind: "click", query_id, document_id, position }

Option A · click_url (empfohlen)

Jeder Treffer enthält click_url über go.querrio.com. Der Link zählt den Klick und leitet sofort auf die gespeicherte Seiten-URL weiter. Gezählt wird nur, wenn der Treffer in genau dieser Suche angezeigt wurde. Keine Cookies, Bots zählen nicht. url bleibt unverändert, falls du direkte Links bevorzugst.

Zero-Click Actions · hit.action

Bei transaktionaler Query-Intent-Klasse (book, tickets, buy, offer, enquire, reserve) erhält der oberste passende Treffer ein Objekt action { kind, label, target_url, action_url }. Intent-Detection deterministisch + LLM, Page-Action beim Crawl aus schema.org potentialAction, CTA-Ankern und URL-Mustern (Confidence ≥ 0.6). Ranking-Bonus max. +0.15, nur bei Basis-Score ≥ 0.3. action_url zählt action_click und leitet per 302 auf das beim Crawl validierte target_url — keine offene Weiterleitung. label ist lokalisiert; rendere es als CTA am Treffer.

<a href="${hit.click_url}" rel="nofollow">${hit.title}</a>

Option B · Ereignisse per API melden

// SDK
client.trackClick(hit, position);          // Klick auf einen Treffer
client.trackSuggestClick(text, hit?.id);   // Klick auf Vorschlag, Tag oder "Häufig gesucht"

// REST (text/plain, damit sendBeacon cross-origin funktioniert)
navigator.sendBeacon("https://api.querrio.com/api/public/v1/track",
  new Blob([JSON.stringify({ key, kind: "click", query_id, document_id: hit.id, position })],
  { type: "text/plain" }));

Felder von POST /api/public/v1/track

FeldPflichtInhalt
keyja*Browser- oder Server-Schlüssel (*alternativ Header x-querrio-key)
kindjaArt des Ereignisses, siehe unten
query_idempfohlenAus der Suchantwort – verknüpft das Ereignis mit der Suche
document_idbei Klickhit.id des Treffers
positionbei KlickPosition in der angezeigten Liste, ab 1
dwell_msbei dwellVerweildauer auf der Zielseite in Millisekunden
session_idoptionalZufällige ID pro Browser-Tab (kein Cookie). Erkennt Umformulierungen und Abbrüche; dieselbe ID auch bei /v1/search mitsenden

Ereignisarten (kind)

  • click – Klick auf einen Treffer (wichtigstes Signal für den Autopilot)
  • suggest_click – Klick auf Vorschlag, Themen-Tag oder „Häufig gesucht“
  • helpful / not_helpful – Antwort auf „War das hilfreich?“
  • dwell – Verweildauer nach dem Klick
  • conversion – Ziel erreicht (z. B. Buchung, Kauf, Anmeldung)
  • reformulation / abandon – Suche umformuliert bzw. ohne Klick verlassen

Antworten

  • 200 ok – gespeichert
  • 400 – kind fehlt oder ist unbekannt
  • 401 / 403 – Schlüssel ungültig bzw. Domain nicht freigegeben
  • 429 – zu viele Meldungen (120 pro Minute und Besucher), Header Retry-After beachten

Tests nicht mitzählen & Datenschutz

  • Testaufrufe (Suche und Tracking) mit dem Server-Schlüssel und dem Header x-querrio-test: 1 senden – sie zählen dann weder in Auswertungen noch im Kontingent. Aufrufe aus dem querrio-Dashboard und von Werkzeugen wie curl mit Browser-Schlüssel werden automatisch als Test erkannt.
  • Keine Cookies, kein Fingerprinting; IP-Adressen werden nicht gespeichert. Für das Tracking ist kein Einwilligungsbanner nötig.
  • Ob Klicks ankommen, zeigt das Backend unter „Einrichtung“ (Klick-Meldung ja/nein).

Auto-Suggest

GET /api/public/v1/suggest?q= liefert Vorschläge aus erfolgreichen früheren Suchen und aus Titeln und Kategorien des Index — also auch direkt nach dem ersten Crawl. Aktivierung, Mindestzeichen, Anzahl und Direkttreffer stellst du im Backend unter „API & Widget“ ein; ist Auto-Suggest aus, antwortet die API mit enabled: false und leeren Listen.

// Typeahead: Vorschläge während der Eingabe
const client = querrio.createClient({ key: "pk_dein_browser_key" });

input.addEventListener("input", async () => {
  const res = await client.suggest(input.value); // ältere Anfragen brechen automatisch ab
  if (!res.enabled) return;                    // im Backend deaktiviert
  render(res.items);  // [{ text: "beispielbegriff", type: "popular" | "content" }]
  render(res.hits);   // Direkttreffer: id, url, title, category, image_url
  showDate(res.date_label); // erkannter Zeitbezug, z. B. "morgen"
});

// Klick auf einen Vorschlag melden (fließt in Analytics und Autopilot):
client.trackSuggestClick(item.text);
client.trackSuggestClick(hit.title, hit.id);

Antwortstruktur

{
  "query": "beispiel suchanfrage",
  "intent": { "topic": "beispielthema", "audience": null },
  "date_from": null, "date_to": null, "date_label": null,
  "top_hits": [
    { "id": "…", "url": "https://…", "title": "Beispiel-Seitentitel",
      "snippet": "…", "image_url": "https://…", "category": "article",
      "tags": ["beispiel"], "score": 0.91, "tier": "top",
      "is_event": false, "event_start": null,
      "event_location": null }
  ],
  "results": [ … ],
  "groups": [ { "category": "article", "label": "Beiträge", "count": 6, "hit_ids": [ … ] } ],
  "suggestions": ["verwandter suchbegriff", "weiterer suchbegriff"],
  "total": 7, "took_ms": 240, "query_id": "…"
}

Debug-Modus

"debug": true (oder ?debug=1) ergänzt die Antwort um ein debug-Objekt: geparster Intent, aufgelöstes Datum/Zielgruppe mit Quelle, aktive Filter und Ranking-Gewichte, Retrieval-Kandidaten vor dem Reranking, Reihenfolge vorher/nachher, Zero-Result-Rescue, Teaser-Schwellen, Score-Breakdown je Treffer und Stage-Timings. Server-Keys: immer erlaubt. Browser-Keys: Freischaltung unter „API & Widget“ nötig, sonst Header x-asky-debug: denied ohne debug-Objekt.

curl -X POST https://api.querrio.com/api/public/v1/search \
  -H 'content-type: application/json' \
  -H 'x-querrio-key: sk_dein_server_key' \
  -d '{ "query": "beispiel suchanfrage", "debug": true }'
"debug": {
  "version": 1,
  "query": { "raw": "beispiel suchanfrage",
             "normalized": "beispiel suchanfrage",
             "language": "de", "limit": 10, "candidate_limit": 30 },
  "intent": { "topic": "beispielthema", "audience": null,
              "entities": { … }, "prompt_override": false },
  "resolved": { "date_from": null, "date_to": null,
                "date_label": null, "date_source": "none",
                "audience": null, "audience_source": "none",
                "now": "2026-04-18T09:12:00Z" },
  "filters": { "date_from": null, "date_to": null,
               "audience": null, "category": null, "tags": [] },
  "weights": { "vector": 0.6, "fts": 0.25, "quality": 0.1,
               "date_boost": 0.15, "audience_boost": 0.1 },
  "candidates": [ { "rank": 1, "id": "…", "url": "https://…",
                    "title": "Beispiel-Seitentitel", "score": 0.83,
                    "is_event": false, "event_start": null } ],
  "rerank":  { "applied": true, "model_used": true, "before": [ … ], "after": [ … ] },
  "rescue":  { "attempted": false, "from_cache": false, "candidates": [], "used": null },
  "tiering": { "top_score": 0.91, "max_top_hits": 2,
               "tier_threshold": 0.12, "top_hit_ids": [ … ] },
  "scores":  [ { "id": "…", "position": 1, "final_score": 0.91,
                 "relative_to_top": 1, "tier": "top", "category": "article" } ],
  "timings": { "config_ms": 12, "intent_embed_ms": 180, "retrieval_ms": 41,
               "rerank_ms": 260, "total_ms": 512 },
  "notes": []
}

Endpunkte

  • POST/api/public/v1/searchIntent-Suche. Liefert Top-Treffer, Liste, Kategorien und Vorschläge.
  • GET/api/public/v1/suggest?q=Typeahead: Vorschläge und Instant-Treffer.
  • GET/api/public/v1/schemaStartzustand: Themen-Tags, Häufig gesucht, Einstiegsseiten, Platzhalter, Kategorien und Sprachen.
  • POST/api/public/v1/trackKlicks, Verweildauer und Feedback melden.
  • GET/api/public/v1/openapi.jsonOpenAPI-3.1-Spezifikation für Codegen.

Limits & Hinweise

  • Rate-Limit: 120 Requests/Min. pro Projekt, mit Browser-Key zusätzlich 30/Min. pro IP. Bei Überschreitung 429 mit Header retry-after.
  • Fehlercodes: 400 fehlender Key, 401 ungültiger Key, 403 Origin nicht erlaubt bzw. Bot, 500 intern.
  • Klick- und Feedback-Events fließen als Lernsignal in das Ranking (Autopilot).
  • Alle Endpunkte senden CORS-Header, ein Proxy ist nicht nötig.