← ONTO GitLab

Guida alla scelta — quando usare ONTO e quando no

ONTO è un motore database a logica trivalente (V/F/N) con paradigma mereologico puro: gli elementi sono aggregati liberi di proprietà, le classi sono selettori intensionali valutati lazy su indici bitset. È un motore di nicchia, eccellente in un insieme preciso di scenari e sbagliato fuori da esso. Questa guida aiuta a capire da che parte si sta.

Diagramma decisionale

Vedi il diagramma decisionale nella pagina di ONTO →

Use case concreti

1. Triage e sorveglianza epidemiologica con dati mancanti

Scenario. Un esame non fatto non è un esame negativo: il sistema deve distinguere "sintomo assente" (F) da "sintomo indeterminato" (N) e non confermare una diagnosi finché i criteri non sono tutti affermati.

Perché ONTO. La trivalenza è nativa: V ^ N = F (un criterio non esaminato non conferma il quadro) mentre F ^ N = N (un criterio negato insieme a uno indeterminato lascia il quadro indeterminato). Con un database booleano ogni regola richiederebbe null-check manuali, e il valore mancante verrebbe comunque silenziato da un default. La membership alle classi cliniche si ricalcola lazy: quando arriva il referto tardivo, il paziente rientra nella classe senza alcuna riscrittura dei dati. Riferimento benchmark: Q4, AND trivalente lazy — 56 volte più veloce di SQLite a 500 000 record.

Esempio completo: examples/diagnosi.onto.

onto
# I sintomi indeterminati restano N ed entrano nelle regole
[ "Mario Rossi"=nome, V=febbre, V=tosse, N=dispnea ] = paziente_1
[ "Anna Bianchi"=nome, V=febbre, F=tosse, V=dispnea ] = paziente_2

# Classe intensionale: si ricalcola a ogni interrogazione, nessuna migrazione
class { V=febbre ^ V=tosse } = SospettiRespiratori
count{ SospettiRespiratori=class } = n_sospetti : int
"sospetti respiratori: {n_sospetti}" : print

# Un esame mancante non conferma il quadro: V ^ N = F
paziente_1.febbre ^ paziente_1.dispnea = quadro : bool
"quadro paziente 1: {quadro}" : print   # F

2. Scoring creditizio e ranking per peso epistemico

Scenario. Valutare richiedenti su una batteria di fattori binari (reddito stabile, storico pagamenti, garanzie), classificare chi manca di alcuni dati senza penalizzarlo come un rifiuto, e produrre una classifica operativa in tempo reale.

Perché ONTO. Il peso epistemico (V=2, N=1, F=0) è mantenuto dal motore come array di interi e il ranking usa selezione O(n), non un ordinamento completo. La distribuzione V/F/N di ogni fattore — utile per audit, monitoraggio del drift e segnalazioni di vigilanza — è un singolo conteggio sui bitset. Riferimento benchmark: Q7 (top-20, 19 volte più veloce di SQLite, circa 130 volte più veloce di un KV tipo Redis) e Q5 (distribuzione, circa 950 volte più veloce di SQLite).

Esempio completo: examples/rischio_credito.onto.

onto
# Ogni fattore di rischio è una proprietà trivalente: N = dato non disponibile
[ "Marco Ferrari"=nome, V=reddito_stabile, V=storico_pagamenti, N=garanzie ] = richiedente_1
[ "Sofia Esposito"=nome, V=reddito_stabile, F=storico_pagamenti, V=garanzie ] = richiedente_2
[ "Ahmed Al-Rashid"=nome, N=reddito_stabile, N=storico_pagamenti, F=garanzie ] = richiedente_3

# Ranking per peso epistemico: V=2, N=1, F=0 — nessuno score ricalcolato a mano
top_k{ 2=k } = migliori : list
"top-2 richiedenti, id e punteggio: {migliori}" : print

# Distribuzione istantanea di un fattore, per audit e monitoraggio
distribution{ "reddito_stabile"=property } = distribuzione : list
"reddito_stabile [V,F,N]: {distribuzione}" : print

3. Classificazione dinamica di inventari e cataloghi

Scenario. Un catalogo prodotti con flag booleani (disponibile, in offerta, spedizione gratuita, top vendita) da segmentare in modo flessibile: oggi "promo spedibili", domani "fuori catalogo ma top vendita", senza toccare lo schema né migrare i dati a ogni nuova segmentazione.

Perché ONTO. Una classe è un selettore sui bitset, non una tabella: si definisce in una riga e aggrega in classe-in-classe (class { Promo ^ F=esaurito }). I prodotti inseriti dopo la definizione ne diventano membri automaticamente; class fixed congela uno snapshot quando serve un punto nel tempo. Riferimento benchmark: Q2-Q3, congiunzioni su 3-6 proprietà — 41-51 volte più veloci di SQLite a 500 000 record.

Esempio completo: examples/negozio.onto.

onto
[ "Monitor 27 4K"=nome, V=disponibile, V=in_offerta, F=spedizione_gratuita ] = p1
[ "Cuffie Wireless"=nome, V=disponibile, V=in_offerta, V=spedizione_gratuita ] = p2

# Segmentazione congiuntiva sui bitset: nessuna JOIN, nessuna migrazione
class { V=disponibile ^ V=in_offerta ^ V=spedizione_gratuita } = PromoSpedibile
count{ PromoSpedibile=class } = n_promo : int
"promo spedibili: {n_promo}" : print   # 1

# Un prodotto inserito dopo la definizione entra nella classe da solo
[ "Hub USB-C"=nome, V=disponibile, V=in_offerta, V=spedizione_gratuita ] = p3
count{ PromoSpedibile=class } = n_dopo : int
"promo spedibili dopo il nuovo inserimento: {n_dopo}" : print   # 2

4. Feature flag e configurazioni sovrapposte

Scenario. Configurazioni a strati (default → ambiente → istanza) e flag funzionali che si combinano: il valore efficace di un flag è la congiunzione/sovrimposizione dei livelli, e validare "nessun flag pericoloso attivo in produzione" deve essere una query, non una review del codice.

Perché ONTO. Ogni strato è un aggregato di proprietà; la forma aggregate X ▷ prop compone e sovrascrive bottom-up senza inheritance né builder pattern, ripetuta per ogni proprietà da aggregare. Le proprietà trivalenti distinguono "flag esplicitamente off" (F) da "flag non definito a questo strato" (N). Esempio completo: examples/configurazione.onto.

onto
# Strati di configurazione: il valore specifico sovrascrive il default
"localhost"=host : str
8080=porta : int
F=cache_abilitata : bool

aggregate config ▷ host
aggregate config ▷ porta
aggregate config ▷ cache_abilitata
"config: {config.host}:{config.porta} cache={config.cache_abilitata}" : print

# Override di uno strato: si dichiara solo la proprietà che cambia
V=cache_abilitata : bool
aggregate config ▷ cache_abilitata
"config dopo override: cache={config.cache_abilitata}" : print

5. Knowledge graph di relazioni tipate

Scenario. Reti di entità collegate da relazioni etichettate (contatti epidemiologici, acquisti correlati, dipendenze tra servizi) da attraversare in avanti e all'indietro sullo stesso dataset delle proprietà.

Perché ONTO. Il grafo di relazioni tipate è integrato nello stesso motore degli elementi: nessun sistema separato da sincronizzare, e le relazioni connettono record che sono già indicizzati trivalentemente. forward/backward rispondono a "chi ha contattato chi" in una traversata diretta. Riferimento benchmark: Q1, scan singolo indicizzato — 6 volte più veloce di SQLite e competitivo con un KV pur dedicato ai lookup.

Esempio completo: examples/motore_db.onto.

onto
[ "Mario"=nome, V=febbre ] = nodo_mario
[ "Anna"=nome, F=febbre ] = nodo_anna
[ "Luca"=nome, V=febbre ] = nodo_luca

# Relazioni tipate tra record dello stesso motore
add_relation{ nodo_mario=from, "contatto"=rel, nodo_anna=to }
add_relation{ nodo_anna=from, "contatto"=rel, nodo_luca=to }

forward{ nodo_mario=from, "contatto"=rel } = catena : list
"catena di contatto da Mario: {catena}" : print

backward{ nodo_luca=to, "contatto"=rel } = origine : list
"chi ha contattato Luca: {origine}" : print

6. Raccomandazione e matching di profili

Scenario. Abbinare utenti a item (corsi, prodotti, contenuti) in base a profili di preferenze booleane, dove "preferenza non espressa" (N) non deve né penalizzare né forzare un match.

Perché ONTO. Il matching per congiunzione trivalente separa dati (gli elementi) da logica (le funzioni di match): aggiungere una nuova dimensione di preferenza è un campo in più negli elementi, non un refactor di schema. Esempio completo: examples/raccomandazione.onto. Riferimento benchmark: Q2-Q3 sul pattern AND multi-proprietà sottostante.

7. Quando NON scegliere ONTO

Scenario Scelta corretta Perché ONTO non va bene
Join tra più tabelle grandi ripartite su più macchine, in cui restano molte righe da unire PostgreSQL e sistemi distribuiti In ONTO la tabella più grande resta nei gruppi, ma le altre vanno raccolte e date a ognuno: devono starci
Cache di sessioni, rate limiting, token con TTL Redis ONTO non ha TTL né eviction
Ricerca per significato come la intende un modello linguistico Motori vettoriali con un modello dedicato Essere un modello linguistico non è tra gli scopi di ONTO: conserva e cerca i vettori calcolati da un modello esterno (nearest), e con similar_text avvicina i testi per le parole che condividono i contesti nell'archivio
Strumenti che dipendono da tutto PostgreSQL: procedure, tipi propri, estensioni, chiavi su più colonne PostgreSQL ONTO parla il protocollo, esegue SQL con tabelle, viste e chiavi esterne ed espone i cataloghi che descrivono lo schema, non il resto

La regola pratica: ONTO quando i dati sono profili booleani con valori mancanti che contano, le query sono congiunzioni e ranking su quei profili, e il deployment è un processo Rust embeddato. Fuori da questa intersezione, usare gli strumenti generalisti.

Limiti e requisiti di produzione

Limiti attuali (onesti)

Limite Implicazione
Persistenza a due livelli Snapshot binario completo (onto run --save/--load, REPL :save/:load) oppure stato persistente con log di scrittura (onto run --store <dir>, onto repl --store <dir>): lo stato è ripristinato all'avvio e ogni elemento o relazione è registrato prima di essere applicato, così un riavvio non perde le scritture. Rieseguire lo stesso script sullo stesso stato non reinserisce i suoi elementi: lo stato ricorda gli script già applicati, e ogni elemento ritrova il record creato la prima volta. Uno script modificato, anche di poco, è uno script nuovo e inserisce tutti i suoi elementi; uno script interrotto da un errore non viene registrato. Il motore non deduplica per profilo: due elementi con le stesse proprietà sono lo stesso ente ma restano determinazioni distinte (LOGICA.md §4.4), e fonderli falserebbe conteggi e classifiche.
Dati ripartiti: in un join una sola tabella resta dov'è In un gruppo di nodi le scritture passano da un primario, che le replica e viene sostituito a maggioranza se cade. onto route ripartisce i dati tra più gruppi, che scrivono insieme. Una scrittura per più gruppi è applicata a tutti o a nessuno: ogni gruppo la prepara, la conserva su disco e presso le sue repliche, e la conclude come gli altri anche se cade l'instradatore o il suo primario. Un campo unico lo è tra tutti i gruppi, per ogni scrittura e con qualunque numero di instradatori: sono i gruppi a controllare e a impegnarsi. Una SELECT dà il risultato di un nodo solo, anche con JOIN e sottointerrogazioni tra gruppi diversi e dentro una transazione aperta, di cui vede le scritture. In un JOIN la tabella più grande resta nei gruppi: le altre, ristrette dalle condizioni dell'interrogazione, sono raccolte e date a ogni gruppo, che le unisce alla sua parte e restituisce un parziale da fondere; le righe della tabella grande non viaggiano. onto split --router aggiunge un gruppo dividendo in due i dati di un altro senza fermare il servizio; i nodi ricordano la disposizione nuova, e ogni instradatore la adotta da solo, anche al riavvio. Restano questi confini: le tabelle di un JOIN diverse dalla più grande devono stare nell'instradatore e in ogni gruppo, e un'interrogazione che usa due volte la stessa tabella, l'id degli elementi o un LEFT JOIN la cui prima tabella è la piccola raccoglie anche la grande; durante una divisione le richieste aspettano che finisca; una scrittura su un campo unico impegna tutti i gruppi, e ogni scrittura su più gruppi è prima provata su una copia dello stato, con un costo che cresce con i dati; i riferimenti tra tabelle (REFERENCES) sono controllati chiedendo a tutti i gruppi prima di scrivere, quindi una scrittura contemporanea può passare tra il controllo e la scrittura, e uno schema cambiato attraverso un instradatore è visto dagli altri entro un secondo; nearest, similar_text e gli altri sorgenti ONTO di lettura sono eseguiti da ogni gruppo e le risposte accostate, non fuse
Oltre la RAM: tutto regge, una lettura sparsa costa un accesso al disco Il motore tiene in memoria tre bit per proprietà più due byte per record (circa 6 byte con 8 proprietà); i testi stanno in memoria una volta sola e gli script sono letti dal disco un'istruzione alla volta. Con --memory-file la memoria del processo sta su un file mappato e il limite diventa il disco. Il sistema è fatto perché ciò che legge e scrive stia vicino: l'allocatore tiene insieme i dati dello stesso genere; inserire un elemento aggiunge in coda alle strutture; l'ordine dei testi di una proprietà si costruisce, da dati già ordinati, solo quando una ricerca per valore o un vincolo di unicità lo chiede; scorrere una tabella legge gli elementi nell'ordine in cui sono scritti; un confronto su una colonna con indice legge dell'indice solo la parte che serve. Misure su 120 000 testi (175 MB), sulla stessa macchina: il carico impiega circa 21 s senza limite, 23 s con 50 MB di RAM e 32 s con 35 MB, un quinto dei dati. Con 35 MB, dopo il carico: scorrere tutti i testi 6 s (meno di 1 s senza limite); la prima ricerca per intervallo su un testo, che ne costruisce l'ordine, 23 s; leggere 2 000 elementi di un intervallo 1,7 s; leggere un elemento qualunque per valore 0,08 s. Quest'ultimo è il costo che resta: un dato che non sta in memoria va letto dal disco, e chi ne legge molti in punti sparsi paga un accesso per ognuno. MotorePartizionato (API Rust) offre un accesso su disco più efficiente per le sole proprietà trivalenti
SQL con tabelle, viste e chiavi esterne; non le procedure né le estensioni di PostgreSQL sql{ ... }, POST /sql e il protocollo di PostgreSQL (--pg-port) eseguono SELECT con espressioni, funzioni, CASE, liste (ARRAY, ANY, ALL), più JOIN, sottointerrogazioni (anche riferite alla riga esterna), WITH e WITH RECURSIVE, funzioni a finestra con cornici, GROUP BY, HAVING, UNION; INSERT, UPDATE, DELETE, anche dentro BEGIN … COMMIT; CREATE TABLE con tipi, NOT NULL, DEFAULT, chiavi, UNIQUE e REFERENCES, tutti imposti a ogni scrittura; ALTER TABLE ... ADD COLUMN, CREATE VIEW. Lo schema si legge da information_schema e dai cataloghi di PostgreSQL che lo descrivono: in psql funzionano \d, \d tabella, \dt, \dv, \dn, \l, \du. Restano fuori le chiavi su più colonne, i vincoli CHECK, le funzioni usate come tabelle, le procedure, i tipi definiti dall'utente e le estensioni; date e orari sono testi. Le classi definite in ONTO non hanno colonne dichiarate: lì una proprietà ha il genere dei valori che riceve. Gli altri cataloghi di PostgreSQL esistono vuoti, e a una domanda sui cataloghi che ONTO non sa interpretare la porta PostgreSQL risponde con un risultato vuoto invece che con un errore: gli strumenti che ne dipendono si collegano, ma mostrano meno di quanto mostrerebbero con PostgreSQL. È provato con psql, psycopg2 e pg8000. Una transazione prova ogni scrittura sullo stato del momento e le applica tutte insieme alla conferma, senza bloccare gli altri
Linguaggio giovane Ecosistema, tooling e superficie di test sono in crescita

Cosa serve per portarlo in produzione

  1. Come servizio: onto serve --store <dir> --token <segreto> --tls-cert <cert> --tls-key <chiave> --workers <n> conserva lo stato su disco (istantanea più diario dei sorgenti), cifra il traffico, chiede il token e serve le letture in parallelo. Con --pg-port <porta> accetta anche i client di PostgreSQL. Per l'alta disponibilità si avviano almeno tre nodi con --node <indirizzo> e --replica verso gli altri: uno scrive, gli altri replicano e lo sostituiscono se cade. Per dividere i dati su più macchine si avviano più gruppi e, davanti, onto route --group <nodi> --group <nodi>. Per aggiungere un gruppo senza fermare il servizio, onto split --router <instradatore> --split <gruppo>=<nodi nuovi>: i nodi ricordano la disposizione nuova e gli instradatori la adottano da soli, anche al riavvio.
  2. Come libreria Rust: onto-rs è una crate; dentro un processo Rust si usa onto_rs::esegui_sorgente(...) oppure direttamente OntoEngine, con MotoreCondiviso per le letture in parallelo.
  3. Come motore di lettura veloce: in architetture con stato condiviso tra più nodi, la via prudente è Postgres come fonte di verità e un motore ONTO ricostruito a intervalli (o da un log) per le query di classificazione e ranking. La costruzione dell'indice è lineare e misurata nel benchmark.
  4. Valutare il carico in anticipo: il benchmark copre il caso rappresentativo (8 proprietà, congiunzioni, ranking); prima di affidargli un dominio nuovo, estendere il dataset di prova con i propri profili e rieseguire cargo run -p onto-cli --release -- bench.

Fonte: onto-rs/docs/USE_CASES.md