Features

RAG-Site-Search-Tools

Zwei Function-Calling-Tools, die das Modell im eigenen öffentlichen Content der Website verankern und dabei den jeweils installierten TYPO3-Suchindex lesen, mit einem Datenbank-Fallback.

Warum es existiert

Agent-Läufe sollen Fragen zum eigenen Content der Website mit belegten Quellen beantworten statt mit dem Weltwissen des Modells. Die Retrieval-Quelle existiert in einer TYPO3-Installation meist schon: EXT:solr, ke_search oder das indexed_search des Cores. Was fehlte, ist eine kontrollierte Retrieval-Schicht, die den jeweils vorhandenen Index nutzt, sauber degradiert, wenn keiner vorhanden ist, und dem Modell ein kuratiertes Evidence-Paket mit auflösbaren Quell-URLs übergibt statt roher Suchtreffer (ADR-049).

Eine generische Tool-Liste pro Engine (solr_search, ke_search_query, …) wurde verworfen: Das Modell müsste wissen, was installiert ist, jede Engine würde ihre eigene Ergebnisform in die Prompts durchsickern lassen, und die Anzahl der Tools würde pro Engine wachsen. Stattdessen bringt nr_llm einen Retrieval-Kern mit vielen Backends hinter einer einzigen Tool-Gruppe mit. Ein Modell im eigenen Content der Website zu verankern, gilt als querschnittliches Primitiv auf einer Stufe mit den Content- und Introspection-Tools – nr_llm darf Indizes lesen, die anderen gehören und die diese aktuell halten, besitzt aber nie einen persistenten Index, den es selbst synchron halten müsste (ADR-050). Vektor-/semantisches RAG bleibt in der Schwester-Extension nr_ai_search.

Wann man es einsetzt

  • Ein Agent muss Fragen zum eigenen öffentlichen Content der Website beantworten und belegen, woher die Antwort stammt, statt sich auf die Trainingsdaten des Modells zu verlassen.
  • Es läuft eines von EXT:solr, ke_search oder indexed_search – die Tools lesen das jeweils installierte in Prioritätsreihenfolge, das erste verfügbare gewinnt.
  • Es läuft keines davon: Der stets verfügbare Datenbank-Fallback über pages/tt_content antwortet trotzdem und wird im Evidence-Header entsprechend gekennzeichnet.
  • Die Belege müssen auf das beschränkt sein, was ein anonymer Besucher lesen könnte – die Filterung auf Indexebene ist immer nur öffentlich (fail-closed ohne Backend-Nutzer).
  • site_rag_query nutzen, um ein kuratiertes Evidence-Paket zu erhalten, dann site_fetch_source(source_id), um den vollständigen indizierten Text einer einzelnen Quelle zu lesen (begrenzt).
  • Für semantisches Retrieval mit persistentem Vector-Store, Chunking und Reindex-on-Change stattdessen nr_ai_search zusätzlich installieren – das liegt hier bewusst außerhalb des Umfangs (ADR-050).

Wie es funktioniert

Ein Retrieval-Kern, viele Backends. Eine Service-/Retrieval-Schicht definiert SearchBackendInterface (getIdentifier, getPriority, isAvailable, search, fetchSource) mit vier Implementierungen, die über den DI-Tag nr_llm.retrieval_backend gesammelt werden. RetrievalService fragt die Backends in Prioritätsreihenfolge ab und nutzt das erste verfügbare – es gibt kein Score-Merging über Engines hinweg, weil Solr-Relevanz, MySQL-Volltext-Scores und LIKE-Treffer nicht vergleichbar sind. Ein Backend, das eine Exception wirft, gilt als nicht verfügbar, und die Kaskade läuft mit einem Vermerk weiter; ein leeres Ergebnis eines verfügbaren Backends ist per Design endgültig. Das antwortende Backend wird im Ergebnis stets genannt, sodass das Modell die Qualität der Belege kennt.

Zwei Tools bilden eine neue Gruppe, rag: site_rag_query (Frage → Evidence-Paket aus source_id · title · url samt einem Treffer-Auszug pro Quelle) und site_fetch_source (source_id → der indizierte Volltext, begrenzt). Das sind 2 der 41 eingebauten, lesenden Tools in 8 schaltbaren Gruppen. Tool-Argumente werden vom Modell gewählt und sind nicht vertrauenswürdig, deshalb greifen Längenbegrenzungen, eine Grammatikprüfung der source-id und Begrenzungen der Ergebnismenge.

Der Zugriff ist fail-closed und ausschließlich öffentlich: Die Filterung auf Indexebene ist immer fe_group ''/0, gr_list 0,-1 und der Solr-Zugriffsfilter {!typo3access}0,-1 – RAG-Belege sind genau das, was der anonyme Besucher lesen könnte. Da dieser Content öffentlich ist, gibt es keine Einschränkung der Seiten pro Nutzer; die Tools setzen dennoch wie jedes eingebaute Tool einen Backend-Nutzer voraus.

SolrSearchBackend

Spricht mit dem EXT:solr-Server über die dokumentierte HTTP-select-API (solr_*_read-Schlüssel der Site-Config, Read-Cores pro Sprache) statt über die @internal-PHP-Klassen von EXT:solr; trägt den öffentlichen Filter {!typo3access}0,-1 und fügt keine Composer-Abhängigkeit zu EXT:solr hinzu.

KeSearchBackend

Liest tx_kesearch_index direkt – MATCH … AGAINST auf MySQL/MariaDB, sonst LIKE; trifft nur title/content, nie hidden_content.

IndexedSearchBackend

Liest die index_*-Tabellen des Cores direkt (Word-Hash-Join mit dem in PHP berechneten md5; LIKE über index_fulltext, wenn die Word-Tabellen leer sind).

DatabaseSearchBackend

Stets verfügbarer Fallback (Priorität 0): LIKE über die Suchfelder von pages/tt_content, gruppiert pro Seite – funktioniert auf einer nackten Instanz ohne Such-Extension.

RetrievalService

Die Kaskade: sortiert die Backends nach Priorität, fragt das erste verfügbare ab, dedupliziert Treffer mit gleicher URL, begrenzt die Anzahl der Quellen und kennzeichnet die Belege mit dem antwortenden Backend.

SiteRagQueryTool / SiteFetchSourceTool

Die beiden eingebauten Tools der rag-Gruppe; sie begrenzen die vom Modell gelieferten Argumente und formatieren die Evidence-Liste in einen zitierbaren Textblock für das Modell.

Für Entwickler

Der Retrieval-Kern lässt sich über einen einzigen DI-Tag erweitern. Der Adapter einer Nischen-Suchmaschine registriert sich über nr_llm.retrieval_backend ohne ein Core-Release; die beiden rag-Tools nutzen RetrievalService und brauchen keine Änderung.

Ein Backend gegen das Interface implementieren

Jedes Backend implementiert SearchBackendInterface. Der Tag wird über AutoconfigureTag automatisch gesetzt, deshalb reicht das Implementieren des Interfaces, um in die Kaskade zu gelangen. isAvailable() muss günstig sein und darf keine Exception werfen; jede Referenz auf Klassen einer optionalen Extension hinter Verfügbarkeitsprüfungen halten, damit die Klasse immer instanziiert werden kann.

php
#[AutoconfigureTag(name: self::TAG_NAME)]
interface SearchBackendInterface
{
    public const TAG_NAME = 'nr_llm.retrieval_backend';

    public function getIdentifier(): string;

    public function getPriority(): int;

    public function isAvailable(): bool;

    public function search(RetrievalQuery $query, AccessContext $context): EvidenceList;

    public function fetchSource(SourceReference $reference, AccessContext $context): ?string;
}

Die Kaskade nach dem Prinzip erstes-verfügbares-gewinnt verstehen

RetrievalService sortiert die Backends nach absteigender Priorität, überspringt nicht verfügbare, behandelt ein Exception werfendes Backend als nicht verfügbar (fügt einen Vermerk hinzu und fährt fort) und gibt das Ergebnis des ersten verfügbaren Backends zurück – dedupliziert nach URL und auf maxSources begrenzt. Es gibt kein Score-Merging über Backends hinweg.

php
foreach ($this->prioritized() as $backend) {
    if (!$this->available($backend)) {
        continue;
    }

    try {
        $result = $backend->search($query, $context);
    } catch (Throwable) {
        $notes[] = sprintf('Backend "%s" failed and was skipped.', $backend->getIdentifier());
        continue;
    }

    return $this->deduplicate($result, $query->maxSources)->withNotes($notes);
}

return new EvidenceList('none', [], [...$notes, 'No search backend available.']);

Die rag-Gruppe wird pro Tool definiert

Ein eingebautes Tool tritt der rag-Gruppe bei, indem es sie aus getGroup() zurückgibt. SiteRagQueryTool begrenzt Fragelänge und Quellenanzahl des Modells, führt RetrievalService mit einem ausschließlich öffentlichen AccessContext für den handelnden Backend-Nutzer aus und formatiert die Belege in einen zitierbaren Textblock.

php
public function getGroup(): string
{
    return 'rag';
}

public function requiresAdmin(): bool
{
    // Evidence is public website content; the backend-user gate above
    // (fail-closed) is the only narrowing needed.
    return false;
}

Rankings fusionieren, wenn man einen eigenen dichten Arm hinzufügt (optional)

Die Kaskade selbst führt Backends nie zusammen, aber hybride Verbraucher, die einen eigenen Embedding-Arm mit der Keyword-Fassade von nr_llm koppeln, können das mitgelieferte Reciprocal-Rank-Fusion-Utility wiederverwenden (ADR-074). Es ist eine reine, newbare finale Klasse – ohne DI, ohne Interface – mit einer Signatur, die mit dem Original aus nr_ai_search identisch ist.

php
$fused = (new ReciprocalRankFusion())->fuse(
    $rankedKeyLists,   // one ranked list of ids per retrieval arm
    k: 60,
    weights: [],
);

Fallstricke

Immer nur öffentlich

Die Filterung auf Indexebene ist fest auf öffentlichen Content gesetzt (fe_group ''/0, gr_list 0,-1, Solr {!typo3access}0,-1). Die Belege sind das, was ein anonymer Besucher lesen könnte; es gibt keine Einschränkung der Seiten pro Nutzer. AccessContext wird durch den Kern durchgereicht, sodass ein künftiger Frontend-Endpunkt die Filterung pro fe_group erweitern könnte, doch das wird in dieser Iteration über das ausschließlich Öffentliche hinaus nicht genutzt.

Das erste verfügbare Backend gewinnt – leer ist endgültig

Ein leeres Ergebnis eines verfügbaren Backends ist kein Grund, zur nächsten Engine durchzufallen; ein Durchfallen würde stillschweigend Engines unterschiedlicher Qualität mischen, und „die Suche der Website findet nichts“ ist selbst ein ehrlicher Beleg. Nur ein nicht verfügbares oder eine Exception werfendes Backend treibt die Kaskade voran.

Direkter Tabellenzugriff kann bei künftigen Major-Versionen driften

KeSearchBackend und IndexedSearchBackend lesen tx_kesearch_index und index_* direkt und tauschen API-Stabilität gegen Entkopplung. Die Schemata sind gegen die derzeit unterstützten Versionen verifiziert (ke_search v6.6/v7, Core 13.4/14.x) und durch funktionale Tests fixiert, aber eine künftige Major-Version könnte driften; isAvailable() prüft, ob die Tabelle vorhanden ist.

Solr nutzt Cores pro Sprache, keinen Sprachfilter

EXT:solr trennt Sprachen über Cores pro Sprache (core_de, core_en, …), deren Schema kein Feld 'language' hat. Ein früheres fq=language:<id> filterte auf ein nicht existierendes Feld und lieferte für jede Abfrage null Ergebnisse; ADR-067 entfernte es – die Sprache wird über die Auswahl des Read-Cores pro Sprache behandelt, der Zugriff allein über {!typo3access}0,-1.

Veraltete Indizes zitieren veralteten Content

Inkrementelle Läufe von ke_search löschen nie, und indexed_search aktualisiert nur beim Rendern, deshalb kann index-basiertes RAG Content zitieren, der sich seither geändert hat. Das ist eine bekannte Eigenschaft index-gestützten Retrievals, für Redakteure dokumentiert – das antwortende Backend wird stets genannt, sodass die Qualität der Belege sichtbar ist.