Blog
Approfondimenti tecnici

Costruire una pipeline RAG ibrida con Reciprocal Rank Fusion

Come la similarità vettoriale e la ricerca full-text PostgreSQL girano in parallelo e si fondono tramite Reciprocal Rank Fusion in risposte con citazioni e punteggi di rilevanza.

Simmetric Team7 min di lettura12/06/2026

Perché una sola strategia di retrieval non basta mai

Il retrieval-augmented generation sta in piedi o cade con ciò che il retriever passa al modello. Quando si pone una domanda a una base di conoscenza, la risposta è buona quanto i chunk che la precedono - e le domande aziendali arrivano in due forme che nessuna strategia di ricerca singola gestisce bene.

Le domande semantiche - "come gestisce la fatturazione la prorata?", "cosa abbiamo deciso sul vendor lock-in?" - riguardano il significato. La ricerca vettoriale eccelle qui: incorpora query e chunk dei documenti nello stesso spazio e recupera i vicini più prossimi, così un chunk scritto con parole completamente diverse può comunque emergere. Ma la ricerca puramente vettoriale è inaffidabile per l'altra forma di domanda: gli identificatori esatti. Codici prodotto, stringhe di errore, numeri di regolamento, nomi di persona - gli embedding li sfumano, e il vicino più prossimo a un codice di errore oscuro è spesso un altro codice, non correlato.

Le domande per parole chiave sono l'immagine speculare. La ricerca full-text di PostgreSQL azzecca i token esatti e i termini rari, ma ignora completamente le parafrasi: chiedete "personale in affitto" e non troverete il documento che dice "risorse esterne".

I documenti aziendali reali attirano entrambi i tipi di domande, spesso nella stessa sessione. Simmetric Chat quindi rifiuta di scegliere. Ogni retrieval esegue due gambe in parallelo - similarità vettoriale e ricerca full-text PostgreSQL - e fonde i risultati con la Reciprocal Rank Fusion (RRF) prima che qualcosa raggiunga il modello. Questo articolo percorre quella pipeline: le due gambe, la matematica della fusione, il percorso di ingestione che le alimenta e lo strato di citazioni che rende le risposte verificabili.

Due gambe, una query

Quando una chat ha bisogno di conoscenza, l'agente ReAct invoca la sua skill rag_search e la ricerca ibrida avvia due retrievals indipendenti allo stesso tempo:

  • La gamba vettoriale lascia il server e chiama il collector, il servizio che possiede lo store vettoriale. Il collector incorpora la query con lo stesso modello di embedding usato al momento dell'ingestione e restituisce i chunk più vicini dalla collezione del workspace. Lo store vettoriale è pluggable - LanceDB per default (file-based, locale, compatibile con l'air-gap), oppure Qdrant, pgvector o Chroma per i deployment che preferiscono un indice server-side.
  • La gamba full-text non lascia mai il server. Interroga PostgreSQL direttamente, confrontando la query con una colonna full-text concatenata sulla tabella dei chunk, supportata da un indice GIN e ordinata per rilevanza lessicale. La tokenizzazione è locale-aware su sette lingue, così i documenti italiani e tedeschi sono ricercabili con le proprie regole di parola anziché con approssimazioni inglesi.

Le collezioni sono per workspace, ed è ciò che mantiene la conoscenza compartimentata: una chat nel workspace A recupera dai chunk del workspace A e da nient'altro. Eseguire entrambe le gambe su ogni query costa un round trip in più e compra una copertura che nessuna delle due ha da sola.

Anche la modalità di guasto è volutamente ridondante. Una guardia confronta il modello di embedding configurato oggi con quello con cui i documenti del workspace sono stati incorporati. In caso di mismatch - per esempio dopo un cambio modello - la gamba vettoriale viene saltata del tutto e il retrieval degrada a solo full-text, anziché servire punteggi di similarità da spazi di embedding incomparabili. La chat continua a funzionare, e l'operatore ottiene un percorso esplicito di re-indicizzazione nel pannello di amministrazione invece di una regressione silenziosa di qualità.

Reciprocal Rank Fusion, in concreto

Le due gambe restituiscono liste ordinate misurate su scale incomensurabili: similarità del coseno da un lato, rilevanza lessicale dall'altro. Mediare i punteggi grezzi significherebbe calibrare quelle scale l'una contro l'altra - fragile, e diverso per ogni modello di embedding e ogni corpus. Simmetric Chat fonde i ranghi anziché i punteggi, usando la Reciprocal Rank Fusion:

text
fused(chunk) = somma sulle gambe di 1 / (k + rank),  con k = 60

Ogni chunk accumula un contributo per ogni gamba in cui compare, così un chunk primo in entrambe le gambe ottiene un punteggio all'incirca doppio rispetto a uno primo in una sola. La costante k smorza l'influenza dei ranghi più alti - con k = 60, la differenza tra il primo e il quinto posto su una singola gamba è un delta piccolo, ed è il consenso tra le gambe a spostare davvero un chunk in cima alla lista fusa.

Due dettagli di ingegneria rendono la fusione di livello produttivo anziché da manuale:

  • Un insieme di sorgenti per chunk registra se è stato trovato dalla gamba vettoriale, da quella full-text o da entrambe. Un chunk recuperato da entrambe è la corroborazione più forte che la pipeline possa produrre, e la formattazione a valle preserva quel segnale invece di appiattirlo.
  • Un tiebreaker deterministico - punteggio fuso decrescente, poi identificativo del documento, poi posizione del chunk - garantisce che la stessa query restituisca lo stesso ordine su ogni replica. Quando qualcuno chiede "perché ha citato questo paragrafo?", la risposta deve essere riproducibile; una fusione non deterministica renderebbe la domanda senza risposta.

Dai documenti ai chunk ricercabili

La fusione è solo metà della pipeline; l'altra metà avviene all'ingestione, quando un documento caricato diventa chunk che entrambe le gambe possono cercare. Il collector analizza file PDF, Word, Excel, PowerPoint, Markdown, CSV e trascrizioni di YouTube, spezza il testo con uno splitter ricorsivo a caratteri (1.000 caratteri per chunk, 200 caratteri di sovrapposizione così le frasi che attraversano un confine non si perdono), incorpora ogni chunk e scrive i vettori nella collezione del workspace.

Fondamentale: il testo dei chunk atterra anche in PostgreSQL, dove il server mantiene il vettore full-text che la gamba FTS interroga. Il collector non tocca mai il database relazionale - un confine di servizio rigido che mantiene sostituibile la pipeline di parsing ed embedding e lascia l'indice di ricerca in mano al server che lo interroga.

La versione 0.22 ha reso il retrieval preciso in una seconda dimensione: il filtraggio per metadati. L'agente può ora limitare una chiamata rag_search ai tipi di documento (PDF, Markdown, TXT, CSV, DOCX, XLSX) e alle finestre di data di ingestione, correggendo il classico guasto "documenti del 2023 che rispondono a domande del 2025". I filtri attraversano entrambe le gambe - come veri pre-filtri sui metadati di ingestione marcati negli store vettoriali, e come predicati SQL parametrizzati sul lato full-text. Un backstop di metadati server-side ricontrolla poi ogni risultato fuso contro la tabella autorevole dei documenti, così la correttezza dei filtri non dipende dal fatto che ogni provider vettoriale implementi i predicati perfettamente. I vettori legacy ingeriti prima della marcatura dei metadati semplicemente escono mentre un filtro è attivo, e riacquistano la filtrabilità tramite un re-embed attivato dall'amministrazione.

Risposte che mostrano il loro lavoro

La qualità del retrieval è solo metà della storia; l'altra metà è l'onestà su cosa la risposta è fondata. Le citazioni scorrono nello stesso canale in streaming dei token: badge di citazione compaiono in linea nella risposta, ognuno con il documento sorgente, il chunk e un punteggio di rilevanza, e ognuno con un link alla fonte così che il lettore possa verificare l'affermazione con un clic.

La disciplina di freschezza vive anche qui. Dopo la deduplicazione dei risultati fusi, un filtro di grounding limita le citazioni per documento alle prime due per punteggio di rilevanza ed elimina le citazioni di chunk il cui testo non condivide una sovrapposizione lessicale significativa con la risposta finale. Il gate è volutamente conservativo - pagine wiki, risultati d'archivio e risposte molto brevi sono sempre conservati - ma elimina la modalità di guasto in cui una risposta dice "nessuna ulteriore informazione" mostrando "Fonti (5)" tratte da un singolo documento appena correlato. Anche l'avviso "nessuna evidenza a supporto" è giudicato sullo stesso insieme filtrato, così l'avviso e la lista delle fonti descrivono sempre la stessa realtà.

Scegliere e regolare lo stack

La pipeline è volutamente guidata dalla configurazione, e i default sono quelli privacy-first:

  • Store vettoriale: LanceDB per default - file-based, conservato localmente, nessun servizio esterno richiesto, pienamente compatibile con l'air-gap. Qdrant, pgvector e Chroma sono a un cambiamento di environment di distanza quando un deployment cresce; il provider è selezionato all'avvio.
  • Embedding: trasformatori locali (384 dimensioni, completamente offline) per default, oppure embedding OpenAI quando un deployment accetta la chiamata in uscita. Cambiare il modello di embedding richiede un re-indicizzazione, guidata dal pannello di amministrazione; la guardia di mismatch tiene il sistema al sicuro nell'attesa.
  • Reranking: opzionale. Un reranker può sovra-recuperare e riordinare i risultati fusi quando la qualità della risposta conta più della latenza.
  • Costante di fusione: k = 60 è il default; è una singola costante nel servizio di ricerca ibrida se le distribuzioni dei vostri ranghi chiedono uno smorzamento diverso.

Il percorso completo del flusso dati - il confine server-collector, il layout di storage dietro ogni provider vettoriale e gli eventi di citazione in streaming - è documentato nella guida all'architettura.

Leggi la guida completa nella documentazione →

Articoli correlati