Aggregazioni e ricerca
Aggregazioni
Sezione intitolata “Aggregazioni”count, sum, avg, min e max sulle righe selezionate da un filtro, senza
doverle recuperare:
GET /api/data/orders/aggregate?select=count(),sum(total){ "data": [{ "count": 128, "sum_total": 40522 }] }Raggruppa per ottenere una riga per valore:
GET /api/data/orders/aggregate?select=count(),sum(total)&groupBy=status{ "data": [ { "status": "paid", "count": 96, "sum_total": 31200 }, { "status": "pending", "count": 32, "sum_total": 9322 } ]}I risultati sono indicizzati per funzione e campo — count() diventa count,
sum(total) diventa sum_total.
I gruppi si paginano come le righe di un elenco. limit li limita, offset (o
page) li salta e orderBy li ordina per un campo di groupBy o per una
chiave del risultato. L’ordine termina sempre sulle chiavi del gruppo, quindi
ogni confine di pagina cade nello stesso punto. Un aggregato raggruppato senza
limit viene troncato al valore predefinito di un elenco (50 gruppi), come un
elenco. Con un limit — incluso quello predefinito — la risposta indica se ce
ne sono altri, e aggregate() dell’SDK lo restituisce come meta nel
risultato:
GET /api/data/orders/aggregate?select=count()&groupBy=customer&orderBy=count:desc&limit=20&offset=20{ "data": [ … ], "meta": { "limit": 20, "offset": 20, "hasMore": true } }offset, page o orderBy senza groupBy, o un cursore su qualsiasi
aggregazione, restituisce 400 INVALID_AGGREGATE_WINDOW: un’aggregazione non
raggruppata ha una sola riga, e un gruppo non è una riga dopo cui un cursore
possa proseguire.
Dall’SDK, aggregate() accetta lo stesso orderBy di un find() — una tupla
[key, "asc" | "desc"] oppure un elenco di tuple — dove la chiave è una colonna di groupBy
o una chiave del risultato. Entrambe sono tipizzate: su un modello generato, una chiave che non è
nessuna delle due non compila. In-process (rebase.data), i gruppi tornano nello
stesso ordine che si ha via HTTP.
const topCustomers = await client.data.orders.aggregate({ select: [{ fn: "count" }, { fn: "sum", field: "total" }], groupBy: ["customer"], orderBy: ["sum_total", "desc"], limit: 10});Accetta gli stessi filtri dell’endpoint di elenco, quindi un’aggregazione può essere ristretta nello stesso modo in cui si filtra un elenco:
GET /api/data/orders/aggregate?select=sum(total)&status=eq.paid&createdAt=gte.2026-01-01Le aggregazioni richiedono un driver che le implementi. In caso contrario, l’endpoint
risponde con 501 invece di un risultato vuoto — una dashboard non dovrebbe
mostrare “nessun risultato” quando in realtà è “non supportato”.
Filtrare all’interno del JSON
Sezione intitolata “Filtrare all’interno del JSON”Una colonna json o jsonb può essere filtrata per percorso (path), usando la sintassi a freccia
di Postgres:
// Ordini i cui metadati indicano che il paese è USconst { data } = await client.data.orders .where("metadata->>country", "==", "US") .find();
// I percorsi nidificati si navigano con -> e si estrae il valore foglia con ->>await client.data.orders.where("metadata->address->>city", "==", "Berlin").find();Tramite REST:
GET /api/data/orders?metadata->>country=eq.USI segmenti del percorso vengono sempre inviati come parametri associati (bound parameters), mai inseriti direttamente nel codice SQL.
Come vengono confrontati i valori
Sezione intitolata “Come vengono confrontati i valori”->> restituisce testo, quindi i confronti sono confronti testuali — tranne quando a un
operatore di ordinamento (>, >=, <, <=) viene passato un numero, nel qual caso esegue il cast a numerico:
await client.data.orders.where("metadata->>score", ">", 100).find(); // numerico: 9 < 100await client.data.orders.where("metadata->>version", ">", "1.2").find(); // testoLe righe il cui valore in quel percorso non è un numero vengono escluse dal
confronto numerico invece di far fallire la query. I booleani vengono confrontati come "true" /
"false", che è il modo in cui ->> li restituisce.
Ricerca di testo
Sezione intitolata “Ricerca di testo”// Tramite parametri findconst { data } = await client.data.products.find({ searchString: "wireless headphones"});
// Stile fluentconst { data } = await client.data.products .search("wireless headphones") .limit(10) .find();Per impostazione predefinita, si tratta di una ricerca per sottostringa senza distinzione tra maiuscole e minuscole (case-insensitive) tra le
proprietà string di primo livello della collezione. Non è una ricerca full-text: non
agisce all’interno delle proprietà map o array, non esegue lo stemming né il ranking, e non può
utilizzare un indice.
Una collezione Postgres può abilitare una vera ricerca full-text dichiarando un
blocco search, che rende inoltre i risultati ordinabili per _score. Consulta
Search.
Ricerca vettoriale
Sezione intitolata “Ricerca vettoriale”Per le collezioni con una proprietà vector, ordina le righe per similarità rispetto a un
embedding di query. Le righe vengono restituite a partire dalle più vicine, ciascuna corredata da una _distance. La
distanza è l’ordinamento: un orderBy sulla stessa query viene ignorato. Senza un
limit una ricerca vettoriale restituisce 10 righe, non le 50 di un elenco.
const { data } = await client.data.docs .vectorSearch("embedding", queryVector, { threshold: 0.35 }) .where("status", "==", "published") .limit(10) .find();where sulla stessa query è un filtro su quali righe sono candidate — questo
restituisce le righe più vicine che soddisfano anche i criteri, non le righe più vicine
filtrate a posteriori. Sull’indice HNSW questo vale da pgvector 0.8.0 (vedi
Filtri su un indice approssimato);
su un pgvector più vecchio un filtro che poche delle righe più vicine superano restituisce una
pagina corta. Generare queryVector è compito tuo: Rebase memorizza e
cerca gli embedding, non li calcola.
Da 0.25 L’SDK invia una
ricerca vettoriale come POST /api/data/<collection>/query, con l’embedding nel
body: nell’URL, 768 dimensioni sono circa 17 KB, oltre ciò che accettano Node (16 KB) e la maggior parte dei
proxy (nginx: 8 KB). count() con una ricerca vettoriale segue la stessa strada,
come POST /api/data/<collection>/count. Contro un server precedente alla 0.24, che
non ha una route del genere, entrambi ripiegano sulla GET — e un embedding lungo lì
può comunque essere rifiutato con un 431 o un 414.
Da 0.25 Con un threshold,
il risultato non ha meta.total — contare le righe entro una distanza lo calcola
per ogni riga della tabella. Pagina con meta.hasMore, che resta esatto.
Cosa devi fornire
Sezione intitolata “Cosa devi fornire”-
pgvector. Una proprietà
vectorviene compilata in una colonnaVECTOR(n), e quel tipo proviene dall’estensionevector. Rebase la installerà per te, ma solo dove indichi che è consentito farlo:// config/resources.tsexport const main = database({ extensions: ["vector"] });Quella riga è un permesso piuttosto che una richiesta — Rebase esegue
CREATE EXTENSION IF NOT EXISTS vectorsolo quando qualcosa nel tuo schema ne ha bisogno. È un’opzione esplicita (opt-in) perché l’installazione di un’estensione dipende da fattori che Rebase non può verificare dall’interno della connessione: l’immagine deve includere la libreria (l’immagine di scaffoldpgvector/pgvector:pg18la include, una standardpostgres:18no), il ruolo deve avere i permessi per installarla, e un provider gestito deve averla inclusa nella allow-list.Se non specifichi nulla, Rebase non installerà niente — puoi in alternativa installarla una volta manualmente. In entrambi i casi la colonna viene creata, e Postgres la rifiuterà con
type "vector" does not existsu un database privo dell’estensione, indicando entrambe le soluzioni possibili.
La colonna, il suo indice ANN e quel CREATE EXTENSION vengono generati all’interno di
.rebase/sql/vector.sql, accanto a schema.sql e policies.sql, e rebase db push li applica automaticamente. Risiedono in un file separato perché Atlas — il
motore alla base di db push — calcola il proprio diff materializzando schema.sql in un
database temporaneo (scratch) che viene cancellato all’inizio di ogni esecuzione, quindi un VECTOR(n) lì dentro
verrebbe risolto a fronte di un database che non può mai avere pgvector.
rebase db generate aggiunge quel file alla migrazione che genera, così una
migrazione rieseguita su un database pulito creerà anche la colonna. Una modifica limitata
alla sola proprietà vettoriale non produce alcuna migrazione, poiché lo schema di cui Atlas calcola il diff
rimane invariato — db generate lo segnala quando accade.
L’indice
Sezione intitolata “L’indice”Ogni colonna vettoriale riceve un indice HNSW per la distanza del coseno, creato insieme alla
tabella e riportato all’avvio. La distanza del coseno viene usata perché è la metrica che vectorSearch utilizza
a meno che non venga specificato distance — un indice supporta esattamente un operatore, quindi una
query l2 eseguita su un indice coseno torna silenziosamente a una scansione sequenziale.
Puoi configurarlo, o disattivarlo, direttamente sulla proprietà:
embedding: { type: "vector", dimensions: 1536, // Valori predefiniti: un indice HNSW, cosine. Ognuno di questi parametri può essere omesso. index: { method: "hnsw", // o "ivfflat" distance: ["cosine", "l2"], // un indice per ciascuno m: 24, // hnsw efConstruction: 128 // hnsw }}index: false mantiene intenzionalmente la scansione esatta. Al di sopra di 2000 dimensioni pgvector
non è in grado di creare nessuno dei due tipi di indice, quindi la colonna viene creata e lasciata non indicizzata,
e il log di avvio lo segnala — vectorSearch risponde comunque, tramite una scansione esatta.
vectorSearch è una query, non una sottoscrizione: invocare .listen() su di essa viene
rifiutato invece di essere gestito come un normale elenco, perché nulla ricalcola le distanze a ogni
operazione di scrittura.
Passaggi successivi
Sezione intitolata “Passaggi successivi”- Interrogare i dati — il query builder su cui queste funzionalità si basano
- Ricerca — come vengono configurate la ricerca full-text e vettoriale sul backend
- API REST — le stesse query tramite HTTP